@flusys/nestjs-auth 1.1.0-beta → 2.0.0-rc.0
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 +590 -473
- package/cjs/config/auth.constants.js +8 -0
- package/cjs/controllers/authentication.controller.js +218 -39
- package/cjs/controllers/branch.controller.js +44 -1
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +44 -1
- package/cjs/controllers/user-permission.controller.js +82 -154
- package/cjs/controllers/user.controller.js +68 -15
- package/cjs/docs/auth-swagger.config.js +14 -7
- package/cjs/dtos/authentication.dto.js +16 -43
- package/cjs/dtos/index.js +0 -1
- package/cjs/dtos/user-permission.dto.js +28 -0
- package/cjs/dtos/user.dto.js +19 -4
- package/cjs/entities/user-company-permission.entity.js +0 -6
- package/cjs/interceptors/clear-token.interceptor.js +10 -23
- package/cjs/interceptors/set-token.interceptor.js +15 -55
- package/cjs/interfaces/auth-email-provider.interface.js +1 -1
- package/cjs/modules/auth.module.js +63 -147
- package/cjs/services/auth-config.service.js +22 -28
- package/cjs/services/auth-datasource.provider.js +9 -21
- package/cjs/services/auth-email.service.js +89 -85
- package/cjs/services/authentication.service.js +225 -396
- package/cjs/services/branch.service.js +9 -4
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/user-permission.service.js +43 -73
- package/cjs/services/user.service.js +78 -64
- package/cjs/strategies/jwt.strategy.js +44 -44
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +2 -0
- package/controllers/authentication.controller.d.ts +5 -3
- package/controllers/company-selection.controller.d.ts +6 -2
- package/controllers/user-permission.controller.d.ts +5 -4
- package/controllers/user.controller.d.ts +6 -0
- package/dtos/authentication.dto.d.ts +0 -4
- package/dtos/company-branch.dto.d.ts +0 -1
- package/dtos/company.dto.d.ts +0 -1
- package/dtos/index.d.ts +0 -1
- package/dtos/user-permission.dto.d.ts +4 -0
- package/dtos/user.dto.d.ts +1 -3
- package/entities/index.d.ts +3 -3
- package/entities/user-company-permission.entity.d.ts +0 -2
- package/fesm/config/auth.constants.js +6 -13
- package/fesm/controllers/authentication.controller.js +221 -42
- package/fesm/controllers/branch.controller.js +45 -2
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +45 -2
- package/fesm/controllers/user-permission.controller.js +84 -156
- package/fesm/controllers/user.controller.js +71 -18
- package/fesm/docs/auth-swagger.config.js +14 -7
- package/fesm/dtos/authentication.dto.js +17 -44
- package/fesm/dtos/index.js +0 -1
- package/fesm/dtos/user-permission.dto.js +28 -0
- package/fesm/dtos/user.dto.js +20 -5
- package/fesm/entities/user-company-permission.entity.js +0 -6
- package/fesm/interceptors/clear-token.interceptor.js +9 -22
- package/fesm/interceptors/set-token.interceptor.js +14 -54
- package/fesm/interfaces/auth-email-provider.interface.js +1 -1
- package/fesm/modules/auth.module.js +64 -148
- package/fesm/services/auth-config.service.js +23 -29
- package/fesm/services/auth-datasource.provider.js +10 -22
- package/fesm/services/auth-email.service.js +90 -86
- package/fesm/services/authentication.service.js +225 -396
- package/fesm/services/branch.service.js +9 -4
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/user-permission.service.js +44 -74
- package/fesm/services/user.service.js +79 -65
- package/fesm/strategies/jwt.strategy.js +44 -44
- package/fesm/validators/password.validator.js +48 -0
- package/interceptors/clear-token.interceptor.d.ts +0 -1
- package/interceptors/set-token.interceptor.d.ts +1 -1
- package/interfaces/authentication.interface.d.ts +0 -4
- package/interfaces/user-enricher.interface.d.ts +1 -4
- package/modules/auth.module.d.ts +6 -3
- package/package.json +4 -4
- package/services/auth-config.service.d.ts +6 -8
- package/services/auth-datasource.provider.d.ts +2 -4
- package/services/auth-email.service.d.ts +8 -4
- package/services/authentication.service.d.ts +15 -16
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/user-permission.service.d.ts +6 -9
- package/services/user.service.d.ts +3 -0
- package/strategies/jwt.strategy.d.ts +4 -1
- package/validators/password.validator.d.ts +10 -0
- package/cjs/dtos/user-company-permission.dto.js +0 -110
- package/dtos/user-company-permission.dto.d.ts +0 -10
- package/fesm/dtos/user-company-permission.dto.js +0 -98
package/README.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# Authentication Package Guide
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flusys/nestjs-auth`
|
|
4
|
-
> **Type:** Authentication system with JWT, multi-tenant, and company/branch support
|
|
5
|
-
|
|
6
|
-
This comprehensive guide covers the authentication package - flexible auth system for NestJS applications.
|
|
4
|
+
> **Type:** Authentication system with JWT, email verification, multi-tenant, and company/branch support
|
|
7
5
|
|
|
8
6
|
## Table of Contents
|
|
9
7
|
|
|
10
8
|
- [Installation](#installation)
|
|
11
9
|
- [Quick Start](#quick-start)
|
|
12
10
|
- [Configuration](#configuration)
|
|
11
|
+
- [Constants](#constants)
|
|
12
|
+
- [Entities](#entities)
|
|
13
|
+
- [Provider Interfaces](#provider-interfaces)
|
|
14
|
+
- [Services](#services)
|
|
13
15
|
- [API Endpoints](#api-endpoints)
|
|
14
|
-
- [Architecture](#architecture)
|
|
15
16
|
- [Login Flows](#login-flows)
|
|
17
|
+
- [Email Verification Flow](#email-verification-flow)
|
|
16
18
|
- [User Management](#user-management)
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [Configuration Modes](#configuration-modes)
|
|
20
|
-
- [IAM Integration](#iam-integration)
|
|
19
|
+
- [No-IAM Mode](#no-iam-mode)
|
|
20
|
+
- [API Reference](#api-reference)
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -40,17 +40,16 @@ import { AuthModule } from '@flusys/nestjs-auth';
|
|
|
40
40
|
AuthModule.forRoot({
|
|
41
41
|
global: true,
|
|
42
42
|
includeController: true,
|
|
43
|
-
includeIAM: true, // Include IAM entities if using @flusys/nestjs-iam
|
|
44
43
|
bootstrapAppConfig: {
|
|
45
44
|
databaseMode: 'single',
|
|
46
45
|
enableCompanyFeature: false,
|
|
47
46
|
},
|
|
48
47
|
config: {
|
|
49
48
|
defaultDatabaseConfig: {
|
|
50
|
-
type: '
|
|
49
|
+
type: 'postgres',
|
|
51
50
|
host: 'localhost',
|
|
52
|
-
port:
|
|
53
|
-
username: '
|
|
51
|
+
port: 5432,
|
|
52
|
+
username: 'postgres',
|
|
54
53
|
password: 'password',
|
|
55
54
|
database: 'myapp',
|
|
56
55
|
},
|
|
@@ -69,20 +68,12 @@ export class AppModule {}
|
|
|
69
68
|
AuthModule.forRoot({
|
|
70
69
|
global: true,
|
|
71
70
|
includeController: true,
|
|
72
|
-
includeIAM: true, // Include IAM entities if using @flusys/nestjs-iam
|
|
73
71
|
bootstrapAppConfig: {
|
|
74
72
|
databaseMode: 'single',
|
|
75
|
-
enableCompanyFeature: true,
|
|
73
|
+
enableCompanyFeature: true,
|
|
76
74
|
},
|
|
77
75
|
config: {
|
|
78
|
-
defaultDatabaseConfig: {
|
|
79
|
-
type: 'mysql',
|
|
80
|
-
host: 'localhost',
|
|
81
|
-
port: 3306,
|
|
82
|
-
username: 'root',
|
|
83
|
-
password: 'password',
|
|
84
|
-
database: 'myapp',
|
|
85
|
-
},
|
|
76
|
+
defaultDatabaseConfig: { /* ... */ },
|
|
86
77
|
jwtSecret: process.env.JWT_SECRET,
|
|
87
78
|
refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET,
|
|
88
79
|
refreshTokenExpiration: '7d',
|
|
@@ -97,17 +88,16 @@ AuthModule.forRoot({
|
|
|
97
88
|
AuthModule.forRoot({
|
|
98
89
|
global: true,
|
|
99
90
|
includeController: true,
|
|
100
|
-
includeIAM: true, // Include IAM entities if using @flusys/nestjs-iam
|
|
101
91
|
bootstrapAppConfig: {
|
|
102
92
|
databaseMode: 'multi-tenant',
|
|
103
93
|
enableCompanyFeature: true,
|
|
104
94
|
},
|
|
105
95
|
config: {
|
|
106
96
|
tenantDefaultDatabaseConfig: {
|
|
107
|
-
type: '
|
|
97
|
+
type: 'postgres',
|
|
108
98
|
host: 'localhost',
|
|
109
|
-
port:
|
|
110
|
-
username: '
|
|
99
|
+
port: 5432,
|
|
100
|
+
username: 'postgres',
|
|
111
101
|
password: 'password',
|
|
112
102
|
},
|
|
113
103
|
tenants: [
|
|
@@ -119,19 +109,36 @@ AuthModule.forRoot({
|
|
|
119
109
|
})
|
|
120
110
|
```
|
|
121
111
|
|
|
122
|
-
###
|
|
112
|
+
### With Email Provider (Password Reset, Email Verification)
|
|
123
113
|
|
|
124
114
|
```typescript
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
115
|
+
import { AUTH_EMAIL_PROVIDER } from '@flusys/nestjs-auth';
|
|
116
|
+
import { EmailSendService } from '@flusys/nestjs-email';
|
|
117
|
+
|
|
118
|
+
AuthModule.forRoot({
|
|
119
|
+
// ... config
|
|
120
|
+
providers: [
|
|
121
|
+
{
|
|
122
|
+
provide: AUTH_EMAIL_PROVIDER,
|
|
123
|
+
useFactory: (emailService: EmailSendService) => ({
|
|
124
|
+
sendPasswordResetEmail: async (email, token, resetUrl) => {
|
|
125
|
+
await emailService.sendTemplateEmail({
|
|
126
|
+
templateSlug: 'password-reset',
|
|
127
|
+
to: email,
|
|
128
|
+
variables: { resetUrl, token },
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
sendVerificationEmail: async (email, token, verifyUrl) => {
|
|
132
|
+
await emailService.sendTemplateEmail({
|
|
133
|
+
templateSlug: 'email-verification',
|
|
134
|
+
to: email,
|
|
135
|
+
variables: { verifyUrl, token },
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
inject: [EmailSendService],
|
|
140
|
+
},
|
|
141
|
+
],
|
|
135
142
|
})
|
|
136
143
|
```
|
|
137
144
|
|
|
@@ -143,356 +150,386 @@ AuthModule.forRootAsync({
|
|
|
143
150
|
interface AuthModuleOptions {
|
|
144
151
|
global?: boolean; // Make module global
|
|
145
152
|
includeController?: boolean; // Include default controllers
|
|
146
|
-
|
|
147
|
-
bootstrapAppConfig: {
|
|
153
|
+
bootstrapAppConfig?: {
|
|
148
154
|
databaseMode: 'single' | 'multi-tenant';
|
|
149
|
-
enableCompanyFeature: boolean;
|
|
155
|
+
enableCompanyFeature: boolean;
|
|
150
156
|
};
|
|
151
|
-
config
|
|
157
|
+
config?: {
|
|
152
158
|
defaultDatabaseConfig?: IDatabaseConfig;
|
|
153
159
|
tenantDefaultDatabaseConfig?: IDatabaseConfig;
|
|
154
160
|
tenants?: ITenantDatabaseConfig[];
|
|
155
|
-
jwtSecret
|
|
156
|
-
jwtExpiration?: string;
|
|
161
|
+
jwtSecret?: string;
|
|
162
|
+
jwtExpiration?: string; // Default: '1h'
|
|
157
163
|
refreshTokenSecret?: string;
|
|
158
|
-
refreshTokenExpiration?: string;
|
|
159
|
-
refreshTokenCookieName?: string;
|
|
164
|
+
refreshTokenExpiration?: string; // Default: '7d'
|
|
165
|
+
refreshTokenCookieName?: string; // Default: 'fsn_refresh_token'
|
|
166
|
+
frontendUrl?: string; // For email links
|
|
160
167
|
};
|
|
168
|
+
providers?: Provider[]; // Additional providers (AUTH_EMAIL_PROVIDER, USER_ENRICHER)
|
|
161
169
|
}
|
|
162
170
|
```
|
|
163
171
|
|
|
164
|
-
|
|
172
|
+
## Constants
|
|
165
173
|
|
|
166
|
-
|
|
174
|
+
```typescript
|
|
175
|
+
// Injection Tokens
|
|
176
|
+
export const AUTH_MODULE_OPTIONS = 'AUTH_MODULE_OPTIONS';
|
|
177
|
+
export const USER_ENRICHER = 'USER_ENRICHER';
|
|
167
178
|
|
|
168
|
-
|
|
179
|
+
// Security
|
|
180
|
+
export const BCRYPT_SALT_ROUNDS = 12;
|
|
169
181
|
|
|
170
|
-
|
|
182
|
+
// Cookie
|
|
183
|
+
export const DEFAULT_COOKIE_NAME = 'fsn_refresh_token';
|
|
184
|
+
export const MAX_COOKIE_BYTES = 3500;
|
|
171
185
|
|
|
172
|
-
|
|
186
|
+
// Cache Key Prefixes
|
|
187
|
+
export const SESSION_CACHE_PREFIX = 'auth:session';
|
|
188
|
+
export const TOKEN_REVOKED_PREFIX = 'auth:revoked';
|
|
189
|
+
```
|
|
173
190
|
|
|
174
|
-
|
|
175
|
-
|----------|--------|-------------|
|
|
176
|
-
| `/auth/login` | POST | Login with email/password |
|
|
177
|
-
| `/auth/register` | POST | Register new user |
|
|
178
|
-
| `/auth/refresh` | POST | Refresh access token |
|
|
179
|
-
| `/auth/logout` | POST | Logout (clears refresh token cookie) |
|
|
180
|
-
| `/auth/change-password` | POST | Change user password |
|
|
181
|
-
| `/auth/me` | GET | Get current user info |
|
|
191
|
+
## Entities
|
|
182
192
|
|
|
183
|
-
|
|
193
|
+
### Entity Groups
|
|
184
194
|
|
|
185
|
-
|
|
195
|
+
```typescript
|
|
196
|
+
// Core entities (always loaded)
|
|
197
|
+
export const CoreEntities = [User, UserToken, PendingRegistration];
|
|
186
198
|
|
|
187
|
-
|
|
199
|
+
// Company entities (when enableCompanyFeature: true)
|
|
200
|
+
export const CompanyEntities = [Company, CompanyBranch, UserCompanyPermission];
|
|
188
201
|
|
|
189
|
-
|
|
202
|
+
// All entities
|
|
203
|
+
export const AllEntities = [...CoreEntities, ...CompanyEntities];
|
|
190
204
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
"user": {
|
|
195
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
196
|
-
"email": "john@example.com",
|
|
197
|
-
"name": "John Doe",
|
|
198
|
-
"profilePictureId": null
|
|
199
|
-
}
|
|
205
|
+
// Helper function
|
|
206
|
+
export function getEntitiesByConfig(enableCompanyFeature: boolean) {
|
|
207
|
+
return enableCompanyFeature ? AllEntities : CoreEntities;
|
|
200
208
|
}
|
|
201
209
|
```
|
|
202
210
|
|
|
203
|
-
|
|
204
|
-
```json
|
|
205
|
-
{
|
|
206
|
-
"user": {
|
|
207
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
208
|
-
"email": "john@example.com",
|
|
209
|
-
"name": "John Doe",
|
|
210
|
-
"profilePictureId": null
|
|
211
|
-
},
|
|
212
|
-
"company": {
|
|
213
|
-
"id": "550e8400-e29b-41d4-a716-446655440001",
|
|
214
|
-
"name": "Acme Corporation",
|
|
215
|
-
"slug": "acme-corp",
|
|
216
|
-
"logoId": null
|
|
217
|
-
},
|
|
218
|
-
"branch": {
|
|
219
|
-
"id": "550e8400-e29b-41d4-a716-446655440002",
|
|
220
|
-
"name": "Main Branch",
|
|
221
|
-
"slug": "main-branch",
|
|
222
|
-
"logoId": null
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
```
|
|
211
|
+
### User Entity
|
|
226
212
|
|
|
227
|
-
|
|
228
|
-
- Uses HTTP GET (not POST like other auth endpoints)
|
|
229
|
-
- Returns only user context data (no tokens)
|
|
230
|
-
- `company` and `branch` fields are automatically excluded from Swagger docs when company feature is disabled
|
|
231
|
-
- Used for session restoration on page refresh (frontend calls this after token refresh)
|
|
213
|
+
Extends `UserRoot` from `@flusys/nestjs-shared`:
|
|
232
214
|
|
|
233
|
-
|
|
215
|
+
| Field | Type | Description |
|
|
216
|
+
|-------|------|-------------|
|
|
217
|
+
| `id` | `uuid` | Primary key |
|
|
218
|
+
| `name` | `varchar(255)` | User's name |
|
|
219
|
+
| `email` | `varchar(255)` | Unique email address |
|
|
220
|
+
| `password` | `text` | Bcrypt hashed password |
|
|
221
|
+
| `phone` | `varchar(255)` | Phone number |
|
|
222
|
+
| `isActive` | `boolean` | Account status |
|
|
223
|
+
| `emailVerified` | `boolean` | Email verification status |
|
|
224
|
+
| `phoneVerified` | `boolean` | Phone verification status |
|
|
225
|
+
| `profilePictureId` | `uuid` | Reference to storage file |
|
|
226
|
+
| `lastLoginAt` | `timestamp` | Last login timestamp |
|
|
227
|
+
| `additionalFields` | `json` | Extensible JSON fields |
|
|
228
|
+
| `createdAt` | `timestamp` | Auto-generated |
|
|
229
|
+
| `updatedAt` | `timestamp` | Auto-updated |
|
|
230
|
+
| `deletedAt` | `timestamp` | Soft delete timestamp |
|
|
234
231
|
|
|
235
|
-
###
|
|
232
|
+
### UserToken Entity
|
|
236
233
|
|
|
237
|
-
|
|
238
|
-
|----------|--------|-------------|
|
|
239
|
-
| `/auth/select` | POST | Select company and branch after login |
|
|
240
|
-
| `/auth/switch-company` | POST | Switch to different company/branch |
|
|
241
|
-
| `/auth/companies` | GET | Get user's available companies |
|
|
242
|
-
| `/auth/companies/:companyId/branches` | GET | Get branches for a company |
|
|
234
|
+
Stores tokens for existing user actions:
|
|
243
235
|
|
|
244
|
-
|
|
236
|
+
```typescript
|
|
237
|
+
enum TokenType {
|
|
238
|
+
PASSWORD_RESET = 'password_reset',
|
|
239
|
+
EMAIL_VERIFICATION = 'email_verification',
|
|
240
|
+
EMAIL_CHANGE = 'email_change',
|
|
241
|
+
}
|
|
242
|
+
```
|
|
245
243
|
|
|
246
|
-
|
|
244
|
+
| Field | Type | Description |
|
|
245
|
+
|-------|------|-------------|
|
|
246
|
+
| `id` | `uuid` | Primary key |
|
|
247
|
+
| `userId` | `uuid` | Reference to user |
|
|
248
|
+
| `type` | `TokenType` | Token purpose |
|
|
249
|
+
| `token` | `varchar(64)` | SHA-256 hashed token |
|
|
250
|
+
| `expiresAt` | `timestamp` | Expiration time |
|
|
251
|
+
| `usedAt` | `timestamp` | When token was used |
|
|
252
|
+
| `metadata` | `varchar(255)` | Additional data (e.g., new email for EMAIL_CHANGE) |
|
|
253
|
+
| `createdAt` | `timestamp` | Auto-generated |
|
|
254
|
+
|
|
255
|
+
**Indexes:**
|
|
256
|
+
- `IDX_user_token_token` - Unique on token
|
|
257
|
+
- `IDX_user_token_user_type` - userId + type
|
|
258
|
+
- `IDX_user_token_expires` - For cleanup queries
|
|
259
|
+
|
|
260
|
+
### PendingRegistration Entity
|
|
261
|
+
|
|
262
|
+
Stores registration data until email verification (prevents garbage accounts):
|
|
263
|
+
|
|
264
|
+
| Field | Type | Description |
|
|
265
|
+
|-------|------|-------------|
|
|
266
|
+
| `id` | `uuid` | Primary key |
|
|
267
|
+
| `email` | `varchar(255)` | Unique pending email |
|
|
268
|
+
| `name` | `varchar(255)` | User's name |
|
|
269
|
+
| `phone` | `varchar(255)` | Phone number |
|
|
270
|
+
| `passwordHash` | `text` | Pre-hashed password |
|
|
271
|
+
| `token` | `varchar(64)` | SHA-256 hashed verification token |
|
|
272
|
+
| `expiresAt` | `timestamp` | Expiration (24 hours) |
|
|
273
|
+
| `companyData` | `json` | Company registration data |
|
|
274
|
+
| `createdAt` | `timestamp` | Auto-generated |
|
|
275
|
+
|
|
276
|
+
**Indexes:**
|
|
277
|
+
- `IDX_pending_reg_email` - Unique
|
|
278
|
+
- `IDX_pending_reg_token` - Unique
|
|
279
|
+
- `IDX_pending_reg_expires` - For cleanup
|
|
280
|
+
|
|
281
|
+
### Company Entity (when company feature enabled)
|
|
282
|
+
|
|
283
|
+
| Field | Type | Description |
|
|
284
|
+
|-------|------|-------------|
|
|
285
|
+
| `id` | `uuid` | Primary key |
|
|
286
|
+
| `name` | `varchar(255)` | Company name |
|
|
287
|
+
| `slug` | `varchar(255)` | URL-friendly identifier |
|
|
288
|
+
| `phone` | `varchar(255)` | Contact phone |
|
|
289
|
+
| `address` | `text` | Address |
|
|
290
|
+
| `logoId` | `uuid` | Reference to storage file |
|
|
291
|
+
| `isActive` | `boolean` | Company status |
|
|
292
|
+
|
|
293
|
+
### CompanyBranch Entity
|
|
294
|
+
|
|
295
|
+
| Field | Type | Description |
|
|
296
|
+
|-------|------|-------------|
|
|
297
|
+
| `id` | `uuid` | Primary key |
|
|
298
|
+
| `name` | `varchar(255)` | Branch name |
|
|
299
|
+
| `slug` | `varchar(255)` | URL-friendly identifier |
|
|
300
|
+
| `companyId` | `uuid` | Parent company |
|
|
301
|
+
| `parentId` | `uuid` | Parent branch (hierarchical) |
|
|
302
|
+
| `serial` | `int` | Display order |
|
|
303
|
+
| `logoId` | `uuid` | Reference to storage file |
|
|
304
|
+
| `isActive` | `boolean` | Branch status |
|
|
305
|
+
|
|
306
|
+
### UserCompanyPermission Entity (Polymorphic)
|
|
247
307
|
|
|
248
|
-
|
|
308
|
+
```typescript
|
|
309
|
+
enum PermissionType {
|
|
310
|
+
COMPANY = 'company',
|
|
311
|
+
BRANCH = 'branch',
|
|
312
|
+
}
|
|
313
|
+
```
|
|
249
314
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
315
|
+
| Field | Type | Description |
|
|
316
|
+
|-------|------|-------------|
|
|
317
|
+
| `id` | `uuid` | Primary key |
|
|
318
|
+
| `userId` | `uuid` | Reference to user |
|
|
319
|
+
| `permissionType` | `PermissionType` | company or branch |
|
|
320
|
+
| `targetId` | `uuid` | Company ID or Branch ID |
|
|
321
|
+
| `isActive` | `boolean` | Permission status |
|
|
255
322
|
|
|
256
|
-
|
|
323
|
+
## Provider Interfaces
|
|
257
324
|
|
|
258
|
-
|
|
259
|
-
|-----|---------------|
|
|
260
|
-
| `RegistrationDto` | `companySlug`, `branchSlug`, `newCompanyName`, `newCompanyPhone`, `newCompanyAddress` |
|
|
261
|
-
| `RegistrationResponseDto` | `company`, `branch`, `companyFeatureEnabled` |
|
|
262
|
-
| `LoginResponseDto` | `company`, `branch`, `companyFeatureEnabled` |
|
|
263
|
-
| `MeResponseDto` | `company`, `branch` |
|
|
325
|
+
### AUTH_EMAIL_PROVIDER
|
|
264
326
|
|
|
265
|
-
|
|
327
|
+
Allows auth to work independently of the email module:
|
|
266
328
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
| `/users/get/:id` | POST | Get user by ID |
|
|
272
|
-
| `/users/get-all` | POST | Get paginated user list |
|
|
273
|
-
| `/users/update` | POST | Update user |
|
|
274
|
-
| `/users/update-many` | POST | Bulk update users |
|
|
275
|
-
| `/users/delete` | POST | Delete/restore/permanent delete |
|
|
276
|
-
| `/users/profile` | POST | Update own profile |
|
|
277
|
-
| `/users/:id/verify-email` | POST | Mark email as verified |
|
|
278
|
-
| `/users/:id/verify-phone` | POST | Mark phone as verified |
|
|
279
|
-
| `/users/:id/status` | PUT | Update user active status |
|
|
280
|
-
|
|
281
|
-
### User Permission Endpoints (NEW - when company feature enabled)
|
|
329
|
+
```typescript
|
|
330
|
+
interface IAuthEmailProvider {
|
|
331
|
+
/** Send password reset email */
|
|
332
|
+
sendPasswordResetEmail(email: string, token: string, resetUrl: string): Promise<void>;
|
|
282
333
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
| `/administration/permissions/user-company/assign` | POST | Batch assign/revoke user-company permissions |
|
|
286
|
-
| `/administration/permissions/user-company` | GET | Get user's assigned companies |
|
|
287
|
-
| `/administration/permissions/user-branch/assign` | POST | Batch assign/revoke user-branch permissions |
|
|
288
|
-
| `/administration/permissions/user-branch` | GET | Get user's assigned branches |
|
|
334
|
+
/** Send email verification email */
|
|
335
|
+
sendVerificationEmail(email: string, token: string, verifyUrl: string): Promise<void>;
|
|
289
336
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
POST /administration/permissions/user-company/assign
|
|
293
|
-
{
|
|
294
|
-
"userId": "user-123",
|
|
295
|
-
"items": [
|
|
296
|
-
{ "targetId": "company-1", "isAdd": true },
|
|
297
|
-
{ "targetId": "company-2", "isAdd": true }
|
|
298
|
-
]
|
|
337
|
+
/** Send welcome email after registration (optional) */
|
|
338
|
+
sendWelcomeEmail?(email: string, name: string): Promise<void>;
|
|
299
339
|
}
|
|
300
340
|
```
|
|
301
341
|
|
|
302
|
-
**Example
|
|
303
|
-
|
|
304
|
-
|
|
342
|
+
**Example Implementation:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
305
345
|
{
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
346
|
+
provide: AUTH_EMAIL_PROVIDER,
|
|
347
|
+
useFactory: (emailSendService: EmailSendService) => ({
|
|
348
|
+
sendPasswordResetEmail: async (email, token, resetUrl) => {
|
|
349
|
+
await emailSendService.sendTemplateEmail({
|
|
350
|
+
templateSlug: 'password-reset',
|
|
351
|
+
to: email,
|
|
352
|
+
variables: { resetUrl, token },
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
sendVerificationEmail: async (email, token, verifyUrl) => {
|
|
356
|
+
await emailSendService.sendTemplateEmail({
|
|
357
|
+
templateSlug: 'email-verification',
|
|
358
|
+
to: email,
|
|
359
|
+
variables: { verifyUrl, token },
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
363
|
+
inject: [EmailSendService],
|
|
310
364
|
}
|
|
311
365
|
```
|
|
312
366
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
### Company Management Endpoints (`/administration/company`) - When `enableCompanyFeature: true`
|
|
316
|
-
|
|
317
|
-
| Endpoint | Method | Description |
|
|
318
|
-
|----------|--------|-------------|
|
|
319
|
-
| `/company/insert` | POST | Create company |
|
|
320
|
-
| `/company/insert-many` | POST | Bulk create companies |
|
|
321
|
-
| `/company/get/:id` | POST | Get company by ID |
|
|
322
|
-
| `/company/get-all` | POST | Get paginated company list |
|
|
323
|
-
| `/company/update` | POST | Update company |
|
|
324
|
-
| `/company/update-many` | POST | Bulk update companies |
|
|
325
|
-
| `/company/delete` | POST | Delete/restore company |
|
|
326
|
-
|
|
327
|
-
### Branch Management Endpoints (`/administration/branch`) - When `enableCompanyFeature: true`
|
|
328
|
-
|
|
329
|
-
| Endpoint | Method | Description |
|
|
330
|
-
|----------|--------|-------------|
|
|
331
|
-
| `/branch/insert` | POST | Create branch |
|
|
332
|
-
| `/branch/insert-many` | POST | Bulk create branches |
|
|
333
|
-
| `/branch/get/:id` | POST | Get branch by ID |
|
|
334
|
-
| `/branch/get-all` | POST | Get paginated branch list (auto-filtered by company) |
|
|
335
|
-
| `/branch/update` | POST | Update branch |
|
|
336
|
-
| `/branch/update-many` | POST | Bulk update branches |
|
|
337
|
-
| `/branch/delete` | POST | Delete/restore branch |
|
|
338
|
-
|
|
339
|
-
## Architecture
|
|
367
|
+
### USER_ENRICHER
|
|
340
368
|
|
|
341
|
-
|
|
369
|
+
Extends user list and profile without modifying base package:
|
|
342
370
|
|
|
371
|
+
```typescript
|
|
372
|
+
interface IUserEnricher {
|
|
373
|
+
/** Enrich user list query with extra joins/selects */
|
|
374
|
+
enrichListQuery?(
|
|
375
|
+
query: SelectQueryBuilder<User>,
|
|
376
|
+
user: ILoggedUserInfo | null,
|
|
377
|
+
): Promise<{ extraFields: string[] }>;
|
|
378
|
+
|
|
379
|
+
/** Transform user list items with extra computed data */
|
|
380
|
+
enrichListItems?(
|
|
381
|
+
users: IUser[],
|
|
382
|
+
user: ILoggedUserInfo | null,
|
|
383
|
+
): Promise<IEnrichedUser[]>;
|
|
384
|
+
|
|
385
|
+
/** Get extra profile data (roles, permissions, sections) */
|
|
386
|
+
getProfileExtras?(
|
|
387
|
+
userId: string,
|
|
388
|
+
user: ILoggedUserInfo,
|
|
389
|
+
): Promise<IProfileExtras>;
|
|
390
|
+
|
|
391
|
+
/** Handle profile update with extra fields */
|
|
392
|
+
updateProfileExtras?(
|
|
393
|
+
userId: string,
|
|
394
|
+
extras: Record<string, any>,
|
|
395
|
+
queryRunner: QueryRunner,
|
|
396
|
+
): Promise<void>;
|
|
397
|
+
|
|
398
|
+
/** Validate extra fields before profile update */
|
|
399
|
+
validateProfileExtras?(
|
|
400
|
+
userId: string,
|
|
401
|
+
extras: Record<string, any>,
|
|
402
|
+
user: ILoggedUserInfo,
|
|
403
|
+
): Promise<void>;
|
|
404
|
+
|
|
405
|
+
// Multi-Step Profile Methods (LEADPRO-style)
|
|
406
|
+
getProfileSections?(): IProfileSection[];
|
|
407
|
+
getProfileSectionData?(userId, sectionId, user): Promise<IProfileSectionData>;
|
|
408
|
+
updateProfileSection?(userId, sectionId, data, queryRunner): Promise<void>;
|
|
409
|
+
handleSectionFileUpload?(userId, sectionId, field, fileInfo, queryRunner): Promise<IProfileFile>;
|
|
410
|
+
handleSectionFileDelete?(userId, sectionId, field, fileId, queryRunner): Promise<void>;
|
|
411
|
+
calculateProfileCompletion?(userId, user): Promise<number>;
|
|
412
|
+
}
|
|
343
413
|
```
|
|
344
|
-
@flusys/nestjs-shared
|
|
345
|
-
├── JwtAuthGuard (validates tokens using Passport)
|
|
346
|
-
└── PermissionGuard (checks permissions from cache)
|
|
347
|
-
↓
|
|
348
|
-
@flusys/nestjs-auth
|
|
349
|
-
├── AuthConfigService (JWT settings via AUTH_MODULE_OPTIONS)
|
|
350
|
-
├── JwtStrategy (registers with Passport, validates users)
|
|
351
|
-
├── AuthenticationService (generates tokens)
|
|
352
|
-
├── User management
|
|
353
|
-
└── Company/Branch management (optional)
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
**Key Points:**
|
|
357
|
-
- ✅ **JwtAuthGuard** in shared → All modules can use without depending on Auth
|
|
358
|
-
- ✅ **JwtStrategy** in auth → Registers JWT validation with Passport
|
|
359
|
-
- ✅ **AuthConfigService** in auth → Provides JWT settings (secret, expiration)
|
|
360
|
-
- ✅ **Token generation** handled by AuthenticationService
|
|
361
|
-
- ✅ **Token validation** uses JwtStrategy registered by Auth module
|
|
362
414
|
|
|
363
|
-
**
|
|
364
|
-
|
|
365
|
-
### JwtStrategy Architecture
|
|
366
|
-
|
|
367
|
-
The `JwtStrategy` is a **singleton** (required by Passport), but needs to access the **request-scoped** `AuthDataSourceProvider`. This is solved using `ModuleRef`:
|
|
415
|
+
**Profile Section Interface (for multi-step profiles):**
|
|
368
416
|
|
|
369
417
|
```typescript
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
passReqToCallback: true, // Pass request to validate method
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async validate(_req: Request, payload: TokenPayload) {
|
|
384
|
-
// Resolve request-scoped AuthDataSourceProvider per request
|
|
385
|
-
const dataSourceProvider = await this.moduleRef.resolve(
|
|
386
|
-
AuthDataSourceProvider,
|
|
387
|
-
undefined,
|
|
388
|
-
{ strict: false },
|
|
389
|
-
);
|
|
418
|
+
interface IProfileSection {
|
|
419
|
+
id: string; // Unique identifier
|
|
420
|
+
name: string; // Display name
|
|
421
|
+
order: number; // Stepper order
|
|
422
|
+
icon?: string; // PrimeNG icon class
|
|
423
|
+
required?: boolean; // Required for completion
|
|
424
|
+
hasFileUpload?: boolean; // Has file fields
|
|
425
|
+
editPermission?: string; // Required permission
|
|
426
|
+
}
|
|
390
427
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
428
|
+
interface IProfileExtras {
|
|
429
|
+
roles?: IProfileRole[];
|
|
430
|
+
actions?: IProfileAction[];
|
|
431
|
+
sections?: IProfileSectionData[];
|
|
432
|
+
completionPercentage?: number;
|
|
394
433
|
}
|
|
395
434
|
```
|
|
396
435
|
|
|
397
|
-
|
|
398
|
-
- ✅ Works for both **single-tenant** and **multi-tenant** modes
|
|
399
|
-
- ✅ `AuthDataSourceProvider` handles tenant detection from request headers
|
|
400
|
-
- ✅ Connection pools are cached internally (no reconnect per request)
|
|
401
|
-
- ✅ Singleton strategy + request-scoped provider = best of both worlds
|
|
402
|
-
|
|
403
|
-
### Package Structure
|
|
404
|
-
|
|
405
|
-
```
|
|
406
|
-
┌─────────────────────────────────────────────────────────┐
|
|
407
|
-
│ @flusys/nestjs-auth v4.1.2+ │
|
|
408
|
-
│ │
|
|
409
|
-
│ ┌─────────────────────────────────────────────────┐ │
|
|
410
|
-
│ │ CORE AUTH (Always Loaded) │ │
|
|
411
|
-
│ │ │ │
|
|
412
|
-
│ │ ┌──────────┐ │ │
|
|
413
|
-
│ │ │ User │ │ │
|
|
414
|
-
│ │ └──────────┘ │ │
|
|
415
|
-
│ │ │ │
|
|
416
|
-
│ │ • User login/registration │ │
|
|
417
|
-
│ │ • Password hashing (bcrypt) │ │
|
|
418
|
-
│ │ • JWT token generation │ │
|
|
419
|
-
│ │ • JwtStrategy (Passport integration) │ │
|
|
420
|
-
│ └─────────────────────────────────────────────────┘ │
|
|
421
|
-
│ │
|
|
422
|
-
│ ┌─────────────────────────────────────────────────┐ │
|
|
423
|
-
│ │ COMPANY FEATURE (Optional - Flag Enabled) │ │
|
|
424
|
-
│ │ │ │
|
|
425
|
-
│ │ ┌─────────┐ ┌──────────────┐ ┌────────────┐ │ │
|
|
426
|
-
│ │ │ Company │ │CompanyBranch │ │UserCompany │ │ │
|
|
427
|
-
│ │ │ │ │ │ │Permission │ │ │
|
|
428
|
-
│ │ └─────────┘ └──────────────┘ └────────────┘ │ │
|
|
429
|
-
│ │ │ │
|
|
430
|
-
│ │ • Multi-company support │ │
|
|
431
|
-
│ │ • Hierarchical branches │ │
|
|
432
|
-
│ │ • User-company-branch assignments │ │
|
|
433
|
-
│ │ • Company-scoped permissions │ │
|
|
434
|
-
│ │ • Enhanced login with selection │ │
|
|
435
|
-
│ └─────────────────────────────────────────────────┘ │
|
|
436
|
-
└─────────────────────────────────────────────────────────┘
|
|
437
|
-
```
|
|
438
|
-
|
|
439
|
-
### Entity Relationships
|
|
440
|
-
|
|
441
|
-
#### Without Company Feature (Simple)
|
|
442
|
-
|
|
443
|
-
```
|
|
444
|
-
┌──────────────┐
|
|
445
|
-
│ User │
|
|
446
|
-
├──────────────┤
|
|
447
|
-
│ id │
|
|
448
|
-
│ name │
|
|
449
|
-
│ email │◄────── Authentication
|
|
450
|
-
│ password │
|
|
451
|
-
│ phone │
|
|
452
|
-
└──────────────┘
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
#### With Company Feature (Polymorphic Design)
|
|
456
|
-
|
|
457
|
-
```
|
|
458
|
-
┌──────────────┐ ┌───────────────────┐ ┌──────────────────┐
|
|
459
|
-
│ User │ │ UserCompany │ │ Company │
|
|
460
|
-
├──────────────┤ │ Permission │ ├──────────────────┤
|
|
461
|
-
│ id │◄──────┤ userId │ │ id │
|
|
462
|
-
│ name │ │ permissionType │──────►│ name │
|
|
463
|
-
│ email │ │ targetId (UUID) │ │ slug │
|
|
464
|
-
│ password │ │ isActive │ │ isActive │
|
|
465
|
-
└──────────────┘ │ metadata │ └──────────────────┘
|
|
466
|
-
└───────────────────┘ │
|
|
467
|
-
│ │
|
|
468
|
-
permissionType: 'company'|'branch' ▼
|
|
469
|
-
targetId: company.id OR branch.id ┌──────────────────┐
|
|
470
|
-
│ │ CompanyBranch │
|
|
471
|
-
└──────────────────►├──────────────────┤
|
|
472
|
-
│ id │
|
|
473
|
-
│ name, companyId │
|
|
474
|
-
│ parentId ◄──┐ │
|
|
475
|
-
│ isActive │ │
|
|
476
|
-
└─────────────┴────┘
|
|
477
|
-
Hierarchical
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
**Polymorphic Permission Design:**
|
|
481
|
-
- `permissionType`: Enum ('company' | 'branch') determines target type
|
|
482
|
-
- `targetId`: UUID of the company OR branch (single field, not both)
|
|
483
|
-
- One table handles both company and branch permissions
|
|
484
|
-
|
|
485
|
-
### Services
|
|
436
|
+
## Services
|
|
486
437
|
|
|
487
438
|
| Service | Scope | Description |
|
|
488
439
|
|---------|-------|-------------|
|
|
489
440
|
| `AuthenticationService` | REQUEST | Login, register, token management, company selection |
|
|
441
|
+
| `AuthEmailService` | REQUEST | Email verification, password reset, pending registrations |
|
|
490
442
|
| `UserService` | REQUEST | User CRUD, extends `RequestScopedApiService` |
|
|
491
443
|
| `CompanyService` | REQUEST | Company CRUD (when company feature enabled) |
|
|
492
444
|
| `BranchService` | REQUEST | Branch CRUD with company filtering |
|
|
493
445
|
| `UserPermissionService` | REQUEST | User-company/branch permission management |
|
|
494
446
|
| `AuthDataSourceProvider` | REQUEST | Dynamic datasource for single/multi-tenant |
|
|
495
447
|
| `AuthConfigService` | SINGLETON | JWT and module configuration access |
|
|
448
|
+
| `CompanySelectionSessionService` | SINGLETON | Session management for company selection |
|
|
449
|
+
|
|
450
|
+
### AuthEmailService
|
|
451
|
+
|
|
452
|
+
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.
|
|
453
|
+
|
|
454
|
+
| Method | Description |
|
|
455
|
+
|--------|-------------|
|
|
456
|
+
| `isEmailEnabled()` | Check if email provider is configured |
|
|
457
|
+
| `hasPendingRegistration(email)` | Check for pending registration |
|
|
458
|
+
| `createPendingRegistration(data)` | Create pending registration with verification email |
|
|
459
|
+
| `forgotPassword(email, resetUrlBase)` | Send password reset email |
|
|
460
|
+
| `resetPassword(token, newPassword)` | Reset password using token |
|
|
461
|
+
| `verifyEmail(token)` | Verify email (pending registration or existing user) |
|
|
462
|
+
| `resendVerificationEmail(email, verifyUrlBase)` | Resend verification email |
|
|
463
|
+
| `cleanupExpired()` | Clean up expired tokens and pending registrations |
|
|
464
|
+
|
|
465
|
+
**Token Expiry:**
|
|
466
|
+
- Password Reset: 1 hour
|
|
467
|
+
- Email Verification: 24 hours
|
|
468
|
+
|
|
469
|
+
## API Endpoints
|
|
470
|
+
|
|
471
|
+
### Authentication Endpoints
|
|
472
|
+
|
|
473
|
+
| Endpoint | Method | Description |
|
|
474
|
+
|----------|--------|-------------|
|
|
475
|
+
| `/auth/login` | POST | Login with email/password |
|
|
476
|
+
| `/auth/register` | POST | Register new user |
|
|
477
|
+
| `/auth/refresh` | POST | Refresh access token |
|
|
478
|
+
| `/auth/logout` | POST | Logout (revokes all tokens) |
|
|
479
|
+
| `/auth/change-password` | POST | Change user password |
|
|
480
|
+
| `/auth/me` | GET | Get current user info |
|
|
481
|
+
|
|
482
|
+
### Email Authentication Endpoints
|
|
483
|
+
|
|
484
|
+
| Endpoint | Method | Description |
|
|
485
|
+
|----------|--------|-------------|
|
|
486
|
+
| `/auth/forgot-password` | POST | Request password reset email |
|
|
487
|
+
| `/auth/reset-password` | POST | Reset password using token |
|
|
488
|
+
| `/auth/verify-email` | POST | Verify email using token |
|
|
489
|
+
| `/auth/resend-verification` | POST | Resend email verification link |
|
|
490
|
+
|
|
491
|
+
> **Note:** Email endpoints require `AUTH_EMAIL_PROVIDER` to be configured.
|
|
492
|
+
|
|
493
|
+
### Company Selection Endpoints (when company feature enabled)
|
|
494
|
+
|
|
495
|
+
| Endpoint | Method | Description |
|
|
496
|
+
|----------|--------|-------------|
|
|
497
|
+
| `/auth/select` | POST | Select company and branch after login |
|
|
498
|
+
| `/auth/switch-company` | POST | Switch to different company/branch |
|
|
499
|
+
| `/auth/companies` | GET | Get user's available companies |
|
|
500
|
+
| `/auth/companies/:companyId/branches` | GET | Get branches for a company |
|
|
501
|
+
|
|
502
|
+
### User Management Endpoints
|
|
503
|
+
|
|
504
|
+
| Endpoint | Method | Description |
|
|
505
|
+
|----------|--------|-------------|
|
|
506
|
+
| `/administration/users/insert` | POST | Create user |
|
|
507
|
+
| `/administration/users/insert-many` | POST | Bulk create users |
|
|
508
|
+
| `/administration/users/get/:id` | POST | Get user by ID |
|
|
509
|
+
| `/administration/users/get-all` | POST | Get paginated user list |
|
|
510
|
+
| `/administration/users/update` | POST | Update user |
|
|
511
|
+
| `/administration/users/update-many` | POST | Bulk update users |
|
|
512
|
+
| `/administration/users/delete` | POST | Delete/restore/permanent delete |
|
|
513
|
+
| `/administration/users/profile` | POST | Update own profile |
|
|
514
|
+
| `/administration/users/:id/verify-email` | POST | Mark email as verified |
|
|
515
|
+
| `/administration/users/:id/verify-phone` | POST | Mark phone as verified |
|
|
516
|
+
| `/administration/users/:id/status` | PUT | Update user active status |
|
|
517
|
+
|
|
518
|
+
### User Permission Endpoints (when company feature enabled)
|
|
519
|
+
|
|
520
|
+
| Endpoint | Method | Description |
|
|
521
|
+
|----------|--------|-------------|
|
|
522
|
+
| `/administration/permissions/user-company/assign` | POST | Batch assign/revoke user-company permissions |
|
|
523
|
+
| `/administration/permissions/user-company` | GET | Get user's assigned companies |
|
|
524
|
+
| `/administration/permissions/user-branch/assign` | POST | Batch assign/revoke user-branch permissions |
|
|
525
|
+
| `/administration/permissions/user-branch` | GET | Get user's assigned branches |
|
|
526
|
+
|
|
527
|
+
### Company/Branch Management Endpoints (when company feature enabled)
|
|
528
|
+
|
|
529
|
+
| Resource | Endpoints |
|
|
530
|
+
|----------|-----------|
|
|
531
|
+
| Companies | `/administration/company` - CRUD operations |
|
|
532
|
+
| Branches | `/administration/branch` - CRUD operations |
|
|
496
533
|
|
|
497
534
|
## Login Flows
|
|
498
535
|
|
|
@@ -507,8 +544,10 @@ POST /auth/login { email, password }
|
|
|
507
544
|
│ .login() │
|
|
508
545
|
└────────────┬────────────┘
|
|
509
546
|
│
|
|
510
|
-
│ 1.
|
|
511
|
-
│ 2.
|
|
547
|
+
│ 1. Check pending registration
|
|
548
|
+
│ 2. Verify credentials
|
|
549
|
+
│ 3. Check email verified (if email enabled)
|
|
550
|
+
│ 4. Generate JWT tokens
|
|
512
551
|
▼
|
|
513
552
|
{ accessToken, refreshToken, user }
|
|
514
553
|
```
|
|
@@ -537,56 +576,148 @@ POST /auth/login { email, password }
|
|
|
537
576
|
POST /auth/login { email, password }
|
|
538
577
|
│
|
|
539
578
|
▼
|
|
540
|
-
┌─────────────────────────┐
|
|
541
|
-
│ AuthenticationService │
|
|
542
|
-
│ .login() │
|
|
543
|
-
└────────────┬────────────┘
|
|
544
|
-
│
|
|
545
|
-
│ Found: Multiple companies/branches
|
|
546
|
-
▼
|
|
547
579
|
{ requiresSelection: true, sessionId, companies: [...] }
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
580
|
+
│
|
|
581
|
+
│ User selects company & branch
|
|
582
|
+
▼
|
|
551
583
|
POST /auth/select { sessionId, companyId, branchId }
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
┌─────────────────────────┐
|
|
555
|
-
│ AuthenticationService │
|
|
556
|
-
│ .select() │
|
|
557
|
-
└────────────┬────────────┘
|
|
558
|
-
│
|
|
559
|
-
│ 1. Validate access via UserCompanyPermission
|
|
560
|
-
│ 2. Generate token with company context
|
|
561
|
-
▼
|
|
584
|
+
│
|
|
585
|
+
▼
|
|
562
586
|
{ accessToken, refreshToken, user, company, branch }
|
|
563
587
|
```
|
|
564
588
|
|
|
589
|
+
### Login with Email Verification Required
|
|
590
|
+
|
|
591
|
+
```
|
|
592
|
+
POST /auth/login { email, password }
|
|
593
|
+
│
|
|
594
|
+
▼
|
|
595
|
+
If pending registration OR email not verified:
|
|
596
|
+
{ requiresEmailVerification: true, email, message }
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Email Verification Flow
|
|
600
|
+
|
|
601
|
+
### New User Registration (with email enabled)
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
605
|
+
│ POST /auth/register { email, password, name } │
|
|
606
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
607
|
+
│
|
|
608
|
+
▼
|
|
609
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
610
|
+
│ 1. Check existing user (must not exist) │
|
|
611
|
+
│ 2. Hash password with bcrypt (SALT_ROUNDS = 12) │
|
|
612
|
+
│ 3. Generate verification token │
|
|
613
|
+
│ 4. Hash token with SHA-256 for storage │
|
|
614
|
+
│ 5. Create PendingRegistration record │
|
|
615
|
+
│ 6. Send verification email via AUTH_EMAIL_PROVIDER │
|
|
616
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
617
|
+
│
|
|
618
|
+
▼
|
|
619
|
+
{ requiresEmailVerification: true, message: "Check your email" }
|
|
620
|
+
│
|
|
621
|
+
│ User clicks email link
|
|
622
|
+
▼
|
|
623
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
624
|
+
│ POST /auth/verify-email { token } │
|
|
625
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
626
|
+
│
|
|
627
|
+
▼
|
|
628
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
629
|
+
│ 1. Hash incoming token with SHA-256 │
|
|
630
|
+
│ 2. Find PendingRegistration by hashed token │
|
|
631
|
+
│ 3. Check expiration │
|
|
632
|
+
│ 4. Delete PendingRegistration record │
|
|
633
|
+
│ 5. Create User with emailVerified: true │
|
|
634
|
+
│ 6. Process company assignment if companyData present │
|
|
635
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
636
|
+
│
|
|
637
|
+
▼
|
|
638
|
+
{ accessToken, refreshToken, user, company?, branch? }
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Password Reset Flow
|
|
642
|
+
|
|
643
|
+
```
|
|
644
|
+
POST /auth/forgot-password { email }
|
|
645
|
+
│
|
|
646
|
+
▼
|
|
647
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
648
|
+
│ 1. Find user by email │
|
|
649
|
+
│ 2. Delete existing PASSWORD_RESET tokens for user │
|
|
650
|
+
│ 3. Generate new token, hash with SHA-256 │
|
|
651
|
+
│ 4. Create UserToken record (expires in 1 hour) │
|
|
652
|
+
│ 5. Send reset email via AUTH_EMAIL_PROVIDER │
|
|
653
|
+
│ 6. Return success (same response even if user not found) │
|
|
654
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
655
|
+
│
|
|
656
|
+
▼
|
|
657
|
+
{ success: true, message: "If email exists, reset link sent" }
|
|
658
|
+
│
|
|
659
|
+
│ User clicks email link
|
|
660
|
+
▼
|
|
661
|
+
POST /auth/reset-password { token, newPassword }
|
|
662
|
+
│
|
|
663
|
+
▼
|
|
664
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
665
|
+
│ 1. Hash incoming token with SHA-256 │
|
|
666
|
+
│ 2. Find UserToken by hashed token + type: PASSWORD_RESET │
|
|
667
|
+
│ 3. Validate not expired, not used │
|
|
668
|
+
│ 4. Update user password │
|
|
669
|
+
│ 5. Mark token as used (usedAt = now) │
|
|
670
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
671
|
+
│
|
|
672
|
+
▼
|
|
673
|
+
{ success: true, message: "Password reset successfully" }
|
|
674
|
+
```
|
|
675
|
+
|
|
565
676
|
## User Management
|
|
566
677
|
|
|
567
|
-
###
|
|
678
|
+
### Company Permission Assignment Flow
|
|
679
|
+
|
|
680
|
+
When creating users with `enableCompanyFeature: true`, permissions are automatically assigned:
|
|
681
|
+
|
|
682
|
+
```
|
|
683
|
+
User Creation (with companyId/branchId)
|
|
684
|
+
│
|
|
685
|
+
▼
|
|
686
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
687
|
+
│ 1. Insert user record │
|
|
688
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
689
|
+
│
|
|
690
|
+
▼
|
|
691
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
692
|
+
│ 2. Check if company permission exists │
|
|
693
|
+
│ - Lookup UserCompanyPermission for user + company │
|
|
694
|
+
│ - If not exists: create { userId, targetId: companyId, │
|
|
695
|
+
│ permissionType: 'company' } │
|
|
696
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
697
|
+
│
|
|
698
|
+
▼
|
|
699
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
700
|
+
│ 3. Check if branch permission exists │
|
|
701
|
+
│ - Lookup UserCompanyPermission for user + branch │
|
|
702
|
+
│ - If not exists: create { userId, targetId: branchId, │
|
|
703
|
+
│ permissionType: 'branch' } │
|
|
704
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
705
|
+
```
|
|
568
706
|
|
|
569
|
-
|
|
707
|
+
### User Active Status
|
|
570
708
|
|
|
571
709
|
**When company feature is disabled:**
|
|
572
710
|
- Updates `User.isActive` directly
|
|
573
|
-
- Simple on/off for the user globally
|
|
574
711
|
|
|
575
712
|
**When company feature is enabled:**
|
|
576
713
|
- Updates `UserCompanyPermission.isActive` for the logged user's company
|
|
577
714
|
- User can be active in Company A but inactive in Company B
|
|
578
|
-
- `getAll` users filters by company permissions
|
|
579
|
-
- `isActive` in responses reflects company permission status
|
|
580
715
|
|
|
581
|
-
### Registration
|
|
716
|
+
### Registration Flows
|
|
582
717
|
|
|
583
718
|
**Simple registration:**
|
|
584
719
|
```json
|
|
585
|
-
{
|
|
586
|
-
"name": "John Doe",
|
|
587
|
-
"email": "john@example.com",
|
|
588
|
-
"password": "password123"
|
|
589
|
-
}
|
|
720
|
+
{ "name": "John Doe", "email": "john@example.com", "password": "password123" }
|
|
590
721
|
```
|
|
591
722
|
|
|
592
723
|
**Join existing company:**
|
|
@@ -607,160 +738,146 @@ The `PUT /users/:id/status` endpoint handles user active status based on company
|
|
|
607
738
|
"email": "john@example.com",
|
|
608
739
|
"password": "password123",
|
|
609
740
|
"newCompanyName": "My Company",
|
|
610
|
-
"
|
|
741
|
+
"newCompanyPhone": "+1234567890",
|
|
742
|
+
"newCompanyAddress": "123 Main St"
|
|
611
743
|
}
|
|
612
744
|
```
|
|
613
745
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
```
|
|
617
|
-
Company: "Acme Corporation"
|
|
618
|
-
│
|
|
619
|
-
├── Main Office (Branch)
|
|
620
|
-
│ ├── Sales Department (Sub-Branch)
|
|
621
|
-
│ │ ├── Inside Sales (Sub-Sub-Branch)
|
|
622
|
-
│ │ └── Outside Sales
|
|
623
|
-
│ └── Support Department
|
|
624
|
-
│ ├── Tier 1 Support
|
|
625
|
-
│ └── Tier 2 Support
|
|
626
|
-
│
|
|
627
|
-
└── Regional Office (Branch)
|
|
628
|
-
├── East Branch
|
|
629
|
-
└── West Branch
|
|
630
|
-
```
|
|
746
|
+
## No-IAM Mode
|
|
631
747
|
|
|
632
|
-
|
|
748
|
+
When not using `@flusys/nestjs-iam` but controllers use `level: 'permission'` security, set wildcard permissions after login:
|
|
633
749
|
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
DB_HOST=localhost
|
|
637
|
-
DB_PORT=3306
|
|
638
|
-
DB_USERNAME=root
|
|
639
|
-
DB_PASSWORD=password
|
|
640
|
-
DB_DATABASE=myapp
|
|
750
|
+
```typescript
|
|
751
|
+
import { HybridCache, CACHE_INSTANCE, PERMISSIONS_CACHE_PREFIX } from '@flusys/nestjs-shared';
|
|
641
752
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
REFRESH_TOKEN_COOKIE_NAME=fsn_refresh_token
|
|
648
|
-
```
|
|
753
|
+
@Injectable()
|
|
754
|
+
export class AuthenticationService {
|
|
755
|
+
constructor(
|
|
756
|
+
@Inject(CACHE_INSTANCE) private readonly cache: HybridCache,
|
|
757
|
+
) {}
|
|
649
758
|
|
|
650
|
-
|
|
759
|
+
async login(dto: LoginDto): Promise<LoginResponse> {
|
|
760
|
+
// ... validate credentials, generate tokens
|
|
651
761
|
|
|
652
|
-
|
|
762
|
+
// When IAM is NOT used, grant all permissions via wildcard
|
|
763
|
+
await this.setWildcardPermissions(user.id);
|
|
653
764
|
|
|
654
|
-
|
|
765
|
+
return { accessToken, user };
|
|
766
|
+
}
|
|
655
767
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
768
|
+
private async setWildcardPermissions(userId: string): Promise<void> {
|
|
769
|
+
const cacheKey = `${PERMISSIONS_CACHE_PREFIX}:user:${userId}`;
|
|
770
|
+
await this.cache.set(cacheKey, ['*'], 86400000); // 24 hours TTL
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
```
|
|
659
774
|
|
|
660
|
-
|
|
661
|
-
npm run migration:run
|
|
775
|
+
### With Company Feature Enabled
|
|
662
776
|
|
|
663
|
-
|
|
664
|
-
|
|
777
|
+
```typescript
|
|
778
|
+
private async setWildcardPermissions(
|
|
779
|
+
userId: string,
|
|
780
|
+
companyId?: string,
|
|
781
|
+
branchId?: string,
|
|
782
|
+
): Promise<void> {
|
|
783
|
+
if (companyId) {
|
|
784
|
+
const cacheKey = `${PERMISSIONS_CACHE_PREFIX}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
|
|
785
|
+
await this.cache.set(cacheKey, ['*'], 86400000);
|
|
786
|
+
} else {
|
|
787
|
+
const cacheKey = `${PERMISSIONS_CACHE_PREFIX}:user:${userId}`;
|
|
788
|
+
await this.cache.set(cacheKey, ['*'], 86400000);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
665
791
|
```
|
|
666
792
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
### Mode 1: Minimal (No Companies)
|
|
793
|
+
### Wildcard Permission Patterns
|
|
670
794
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
})
|
|
795
|
+
| Pattern | Meaning |
|
|
796
|
+
|---------|---------|
|
|
797
|
+
| `*` | Full access to everything |
|
|
798
|
+
| `user.*` | All user permissions |
|
|
799
|
+
| `branch.*` | All branch permissions |
|
|
677
800
|
|
|
678
|
-
|
|
679
|
-
Tables: 1
|
|
680
|
-
Features: Basic login, registration
|
|
681
|
-
Size: Minimal
|
|
682
|
-
Performance: Fast
|
|
683
|
-
```
|
|
801
|
+
## API Reference
|
|
684
802
|
|
|
685
|
-
###
|
|
803
|
+
### Exports
|
|
686
804
|
|
|
687
805
|
```typescript
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
bootstrapAppConfig: {
|
|
691
|
-
enableCompanyFeature: true,
|
|
692
|
-
},
|
|
693
|
-
})
|
|
806
|
+
// Config
|
|
807
|
+
export { AUTH_MODULE_OPTIONS, BCRYPT_SALT_ROUNDS, DEFAULT_COOKIE_NAME, MAX_COOKIE_BYTES, SESSION_CACHE_PREFIX, TOKEN_REVOKED_PREFIX } from './config';
|
|
694
808
|
|
|
695
|
-
Entities
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
809
|
+
// Entities
|
|
810
|
+
export { User } from './entities/user.entity';
|
|
811
|
+
export { UserToken, TokenType } from './entities/user-token.entity';
|
|
812
|
+
export { PendingRegistration } from './entities/pending-registration.entity';
|
|
813
|
+
export { Company } from './entities/company.entity';
|
|
814
|
+
export { CompanyBranch } from './entities/company-branch.entity';
|
|
815
|
+
export { UserCompanyPermission, PermissionType } from './entities/user-company-permission.entity';
|
|
816
|
+
export { CoreEntities, CompanyEntities, AllEntities, getEntitiesByConfig } from './entities';
|
|
702
817
|
|
|
703
|
-
|
|
818
|
+
// Enums
|
|
819
|
+
export { PermissionType } from './enums';
|
|
704
820
|
|
|
705
|
-
|
|
821
|
+
// Interfaces
|
|
822
|
+
export { IAuthEmailProvider, AUTH_EMAIL_PROVIDER } from './interfaces/auth-email-provider.interface';
|
|
823
|
+
export { IUserEnricher, IProfileSection, IProfileExtras, IEnrichedUser } from './interfaces/user-enricher.interface';
|
|
824
|
+
export { AuthModuleOptions, AuthModuleAsyncOptions, AuthOptionsFactory } from './interfaces/auth-module-options.interface';
|
|
825
|
+
export { TokenPayload, TokenPayloadWithCompany, LoginResponse, MeResponse, RegistrationResponse } from './interfaces/authentication.interface';
|
|
706
826
|
|
|
707
|
-
|
|
827
|
+
// Services
|
|
828
|
+
export { AuthenticationService } from './services/authentication.service';
|
|
829
|
+
export { AuthEmailService } from './services/auth-email.service';
|
|
830
|
+
export { AuthConfigService } from './services/auth-config.service';
|
|
831
|
+
export { AuthDataSourceProvider } from './services/auth-datasource.provider';
|
|
832
|
+
export { UserService } from './services/user.service';
|
|
833
|
+
export { CompanyService } from './services/company.service';
|
|
834
|
+
export { BranchService } from './services/branch.service';
|
|
835
|
+
export { UserPermissionService } from './services/user-permission.service';
|
|
836
|
+
export { CompanySelectionSessionService } from './services/company-selection-session.service';
|
|
708
837
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// Auth module calls TypeOrmModule.forRoot() with auth entities
|
|
712
|
-
TypeOrmModule.forRoot({ entities: [User, Company, ...] })
|
|
838
|
+
// Modules
|
|
839
|
+
export { AuthModule } from './modules/auth.module';
|
|
713
840
|
|
|
714
|
-
//
|
|
715
|
-
|
|
716
|
-
```
|
|
841
|
+
// Strategies
|
|
842
|
+
export { JwtStrategy } from './strategies/jwt.strategy';
|
|
717
843
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
})
|
|
844
|
+
// Interceptors
|
|
845
|
+
export { SetToken } from './interceptors/set-token.interceptor';
|
|
846
|
+
export { ClearToken } from './interceptors/clear-token.interceptor';
|
|
847
|
+
|
|
848
|
+
// DTOs
|
|
849
|
+
export * from './dtos';
|
|
725
850
|
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
entities: [User, Company, Action, Role, Permission, ...]
|
|
729
|
-
}) // ✅ Works!
|
|
851
|
+
// Swagger Config
|
|
852
|
+
export { authSwaggerConfig } from './docs/auth-swagger.config';
|
|
730
853
|
```
|
|
731
854
|
|
|
732
|
-
|
|
733
|
-
- ✅ Single-tenant mode + IAM module → `includeIAM: true`
|
|
734
|
-
- ❌ Multi-tenant mode → Not needed (each tenant has own connection)
|
|
735
|
-
- ❌ Not using IAM → `includeIAM: false` (default)
|
|
855
|
+
### Module Static Methods
|
|
736
856
|
|
|
737
|
-
|
|
857
|
+
```typescript
|
|
858
|
+
// Get entities based on feature flags
|
|
859
|
+
AuthModule.getEntities(enableCompanyFeature?: boolean): any[]
|
|
738
860
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
│ └── TypeOrmModule.forRoot([Auth entities + IAM entities])
|
|
743
|
-
│
|
|
744
|
-
└── IAMModule.forRoot()
|
|
745
|
-
└── TypeOrmModule.forFeature([IAM entities])
|
|
861
|
+
// Configure module
|
|
862
|
+
AuthModule.forRoot(options: AuthModuleOptions): DynamicModule
|
|
863
|
+
AuthModule.forRootAsync(options: AuthModuleAsyncOptions): DynamicModule
|
|
746
864
|
```
|
|
747
865
|
|
|
748
|
-
|
|
866
|
+
### Swagger Schema Behavior
|
|
749
867
|
|
|
750
|
-
|
|
868
|
+
When `enableCompanyFeature: false`, the following are automatically hidden from Swagger:
|
|
751
869
|
|
|
752
|
-
|
|
870
|
+
**Excluded Tags:** Companies, Branches, User Permissions, Company Selection
|
|
753
871
|
|
|
754
|
-
|
|
872
|
+
**Excluded DTO Properties:**
|
|
755
873
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
- **Performance** - Only load what you need
|
|
874
|
+
| DTO | Hidden Fields |
|
|
875
|
+
|-----|---------------|
|
|
876
|
+
| `RegistrationDto` | `companySlug`, `branchSlug`, `newCompanyName`, `newCompanyPhone`, `newCompanyAddress` |
|
|
877
|
+
| `RegistrationResponseDto` | `company`, `branch`, `companyFeatureEnabled` |
|
|
878
|
+
| `LoginResponseDto` | `company`, `branch`, `companyFeatureEnabled` |
|
|
879
|
+
| `MeResponseDto` | `company`, `branch` |
|
|
763
880
|
|
|
764
881
|
---
|
|
765
882
|
|
|
766
|
-
**Last Updated:** 2026-02-
|
|
883
|
+
**Last Updated:** 2026-02-21
|