@flusys/ng-auth 3.0.0-rc → 3.0.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
@@ -5,6 +5,7 @@
5
5
  `@flusys/ng-auth` provides complete authentication functionality including login, registration, token management, company/branch selection, user administration, and permission handling.
6
6
 
7
7
  **Package:** `@flusys/ng-auth`
8
+ **Version:** 3.0.1
8
9
  **Dependencies:** ng-core, ng-shared, ng-layout
9
10
  **Build:** `npm run build:ng-auth`
10
11
 
@@ -27,18 +28,21 @@
27
28
  │ Guards │
28
29
  │ ├── authGuard (requires authentication) │
29
30
  │ ├── guestGuard (allows only unauthenticated) │
30
- │ └── companyFeatureGuard (requires company feature enabled) │
31
+ │ ├── companyFeatureGuard (requires company feature enabled) │
32
+ │ ├── emailFeatureGuard (requires email service enabled) │
33
+ │ └── signUpFeatureGuard (requires sign-up enabled) │
31
34
  ├─────────────────────────────────────────────────────────────┤
32
35
  │ Interceptors │
33
36
  │ ├── authInterceptor (adds JWT + tenant headers) │
34
- │ └── tokenRefreshInterceptor (handles 401 + token refresh) │
37
+ │ └── tokenRefreshInterceptor (proactive + reactive refresh) │
35
38
  ├─────────────────────────────────────────────────────────────┤
36
39
  │ Adapters (Provider Interface Pattern) │
37
40
  │ ├── AuthLayoutStateAdapter → LAYOUT_AUTH_STATE │
38
41
  │ ├── AuthLayoutApiAdapter → LAYOUT_AUTH_API │
39
42
  │ ├── AuthUserProviderAdapter → USER_PROVIDER │
40
43
  │ ├── AuthCompanyApiProviderAdapter → COMPANY_API_PROVIDER │
41
- │ └── AuthUserPermissionProviderAdapter → USER_PERMISSION_PROVIDER │
44
+ │ ├── AuthUserPermissionProviderAdapter → USER_PERMISSION_PROVIDER │
45
+ │ └── AuthStateProviderAdapter → AUTH_STATE_PROVIDER │
42
46
  ├─────────────────────────────────────────────────────────────┤
43
47
  │ Constants │
44
48
  │ └── AUTH_ENDPOINTS (endpoint path definitions) │
@@ -51,7 +55,7 @@
51
55
 
52
56
  ### AuthApiService
53
57
 
54
- HTTP client for authentication endpoints.
58
+ HTTP client for authentication endpoints. Uses `getServiceUrl(appConfig, 'auth')` for base URL.
55
59
 
56
60
  ```typescript
57
61
  import { AuthApiService } from '@flusys/ng-auth';
@@ -59,26 +63,55 @@ import { AuthApiService } from '@flusys/ng-auth';
59
63
  const authApi = inject(AuthApiService);
60
64
 
61
65
  // Authentication
62
- authApi.login({ email, password }); // POST /auth/login
63
- authApi.register({ name, email, password }); // POST /auth/register
64
- authApi.refresh(); // POST /auth/refresh
65
- authApi.logout(); // POST /auth/logout
66
- authApi.me(); // GET /auth/me
66
+ authApi.login({ email, password }); // POST /auth/login → AuthResponse
67
+ authApi.register({ name, email, password, ... }); // POST /auth/register → IRegistrationResponse
68
+ authApi.refresh(); // POST /auth/refresh → IRefreshTokenResponse
69
+ authApi.logout(); // POST /auth/logout → IMessageResponse
70
+ authApi.me(); // POST /auth/me → IMeResponse
67
71
 
68
72
  // Company/Branch selection
69
- authApi.select({ companyId, branchId, sessionId }); // POST /auth/select
70
- authApi.switchCompany({ companyId, branchId }); // POST /auth/switch-company
73
+ authApi.select({ companyId, branchId, sessionId }); // POST /auth/select → ILoginResponse
74
+ authApi.switchCompany({ companyId, branchId }); // POST /auth/switch-company → ILoginResponse
71
75
 
72
76
  // Password management
73
- authApi.changePassword({ currentPassword, newPassword });
74
- authApi.forgotPassword({ email });
75
- authApi.resetPassword({ token, newPassword });
77
+ authApi.changePassword({ currentPassword, newPassword }); // POST /auth/change-password
78
+ authApi.forgotPassword(email); // POST /auth/forgot-password
79
+ authApi.resetPassword(token, newPassword); // POST /auth/reset-password
80
+
81
+ // Email verification
82
+ authApi.verifyEmail(token); // POST /auth/verify-email
83
+ authApi.resendVerification(email); // POST /auth/resend-verification
84
+ ```
85
+
86
+ **Response Type Guards:**
87
+ ```typescript
88
+ import {
89
+ isEmailVerificationRequired,
90
+ isCompanySelectionResponse,
91
+ isLoginResponse,
92
+ AuthResponse
93
+ } from '@flusys/ng-auth';
94
+
95
+ // AuthResponse can be ILoginResponse | ICompanySelectionResponse | ILoginRequiresEmailVerification
96
+ const response = await firstValueFrom(authApi.login(credentials));
97
+
98
+ if (isEmailVerificationRequired(response)) {
99
+ // Handle email not verified
100
+ }
101
+ if (isCompanySelectionResponse(response)) {
102
+ // Handle company selection step
103
+ }
104
+ if (isLoginResponse(response)) {
105
+ // Direct login success
106
+ }
76
107
  ```
77
108
 
78
109
  ### AuthStateService
79
110
 
80
111
  Signal-based centralized auth state management with auto-persistence.
81
112
 
113
+ **Security Note:** Access tokens are stored in memory only (not localStorage) to prevent XSS attacks. Only token expiry time is persisted.
114
+
82
115
  ```typescript
83
116
  import { AuthStateService } from '@flusys/ng-auth';
84
117
 
@@ -86,30 +119,47 @@ const authState = inject(AuthStateService);
86
119
 
87
120
  // Read-only signals
88
121
  authState.user(); // Signal<IUserInfo | null>
89
- authState.accessToken(); // Signal<string | null>
122
+ authState.accessToken(); // Signal<string> (memory-only)
123
+ authState.tokenExpiresAt(); // Signal<number | null> (milliseconds timestamp)
90
124
  authState.company(); // Signal<ICompanyInfo | null>
91
125
  authState.branch(); // Signal<IBranchInfo | null>
92
- authState.availableCompanies(); // Signal<ICompanyWithBranches[]>
93
126
  authState.selectionSessionId(); // Signal<string | null>
127
+ authState.availableCompanies(); // Signal<ICompanyWithBranches[]>
94
128
 
95
129
  // Computed signals
96
- authState.isAuthenticated(); // Signal<boolean>
130
+ authState.isAuthenticated(); // Signal<boolean> - has token AND user
97
131
  authState.requiresCompanySelection(); // Signal<boolean>
98
132
  authState.userName(); // Signal<string>
99
133
  authState.companyName(); // Signal<string>
100
134
  authState.branchName(); // Signal<string>
101
135
  authState.tenantId(); // Signal<string | null>
136
+ authState.isTokenExpired(); // Signal<boolean> - true if expired or expiring within 30s
102
137
 
103
138
  // State mutations
104
139
  authState.setLoginState(response); // After successful login
105
- authState.setSelectionState(response); // After company/branch selection
140
+ authState.setSelectionState(response); // After login requires selection
106
141
  authState.clearSelectionState(); // Clear pending selection
107
- authState.resetState(); // Full logout
142
+ authState.resetState(); // Full logout - clears all state + localStorage
143
+
144
+ // Individual setters
145
+ authState.setUser(user);
146
+ authState.setAccessToken(token); // Auto-decodes JWT to extract expiry
147
+ authState.setCompany(company);
148
+ authState.setBranch(branch);
149
+
150
+ // Navigation helpers
108
151
  authState.navigateToLogin(returnUrl?);
109
- authState.navigateAfterLogin();
152
+ authState.navigateAfterLogin(returnUrl?); // Validates URL to prevent open redirect
153
+
154
+ // Storage accessors
155
+ authState.getStoredCompanyId(); // Get persisted company ID
156
+ authState.getStoredBranchId(); // Get persisted branch ID
110
157
  ```
111
158
 
112
- **Auto-persistence:** Tokens and company/branch IDs are automatically saved to localStorage via effects.
159
+ **Auto-persistence via effects:**
160
+ - `tokenExpiresAt` → localStorage `tokenExpiresAt`
161
+ - `company.id` → localStorage `selectedCompanyId`
162
+ - `branch.id` → localStorage `selectedBranchId`
113
163
 
114
164
  ### AuthInitService
115
165
 
@@ -121,22 +171,45 @@ import { AuthInitService } from '@flusys/ng-auth';
121
171
  const authInit = inject(AuthInitService);
122
172
 
123
173
  // Signals
124
- authInit.initialized(); // Signal<boolean>
174
+ authInit.initialized(); // Signal<boolean> - true after init attempt
125
175
  authInit.loading(); // Signal<boolean>
126
176
  authInit.error(); // Signal<string | null>
127
177
 
128
- // Restore session (called by appInitGuard)
178
+ // Restore session (called by guards)
129
179
  authInit.initialize().subscribe({
130
- next: (user) => console.log('Session restored:', user),
180
+ next: (success) => console.log('Session restored:', success),
131
181
  error: (err) => console.log('No session or expired')
132
182
  });
133
183
  ```
134
184
 
135
- **Flow:**
136
- 1. Attempts `POST /auth/refresh` with httpOnly cookie
137
- 2. On success, calls `GET /auth/me` to get user info
138
- 3. Updates AuthStateService with restored session
139
- 4. Caches result to prevent duplicate calls
185
+ **Initialization Flow:**
186
+ 1. Returns immediately if already authenticated with user data
187
+ 2. Returns cached result if already initialized (prevents retry loops)
188
+ 3. Reuses in-flight initialization (shareReplay)
189
+ 4. Attempts `POST /auth/refresh` with httpOnly cookie (5s timeout)
190
+ 5. On success, calls `POST /auth/me` to get user info
191
+ 6. Updates AuthStateService with restored session
192
+ 7. Handles company assignment errors gracefully
193
+ 8. Clears cache on finalize to allow re-initialization later
194
+
195
+ ### TokenRefreshStateService
196
+
197
+ Manages concurrent token refresh with request queuing.
198
+
199
+ ```typescript
200
+ import { TokenRefreshStateService } from '@flusys/ng-auth';
201
+
202
+ const refreshState = inject(TokenRefreshStateService);
203
+
204
+ // Signals
205
+ refreshState.isRefreshing(); // Signal<boolean>
206
+ refreshState.refreshTokenSubject$; // Observable<string | null> - for queuing
207
+
208
+ // State management
209
+ refreshState.startRefresh(); // Begin refresh, set subject to null
210
+ refreshState.completeRefresh(token); // Complete with new token
211
+ refreshState.failRefresh(); // Mark failed
212
+ ```
140
213
 
141
214
  ### Resource API Services
142
215
 
@@ -149,32 +222,34 @@ import { UserApiService, CompanyApiService, BranchApiService } from '@flusys/ng-
149
222
  service.data(); // Signal<T[]>
150
223
  service.isLoading(); // Signal<boolean>
151
224
  service.error(); // Signal<string | null>
225
+ service.total(); // Signal<number>
152
226
 
153
227
  // Common methods
154
- await service.insertAsync(data); // Create
155
- await service.updateAsync(data); // Update
156
- await service.deleteAsync(id); // Delete
157
- await service.fetchByIdAsync(id); // Get one
158
- service.fetchList(search, options); // Get list with pagination
228
+ await service.insertAsync(data);
229
+ await service.updateAsync(data);
230
+ await service.deleteAsync({ id, type: 'delete' });
231
+ await service.findByIdAsync(id);
232
+ service.fetchList(search, options);
233
+ service.reload();
159
234
  ```
160
235
 
161
236
  **UserApiService** - Endpoint: `administration/users`
162
237
  ```typescript
163
- userApi.verifyEmail(userId);
164
- userApi.verifyPhone(userId);
165
- userApi.updateStatus(userId, status);
166
- userApi.updateProfile(data);
238
+ const userApi = inject(UserApiService);
239
+ userApi.updateProfile(data); // POST /administration/users/profile
167
240
  ```
168
241
 
169
242
  **CompanyApiService** - Endpoint: `administration/company`
243
+ ```typescript
244
+ const companyApi = inject(CompanyApiService);
245
+ // Standard CRUD via ApiResourceService
246
+ ```
170
247
 
171
248
  **BranchApiService** - Endpoint: `administration/branch`
172
249
  ```typescript
173
- branchApi.currentCompanyId(); // Signal for company context
174
- branchApi.companyBranches(); // Computed: branches filtered by company
175
- branchApi.setCurrentCompanyId(id);
176
- branchApi.insertBranch(data); // Auto-sets companyId
177
- branchApi.fetchByCompany(companyId);
250
+ const branchApi = inject(BranchApiService);
251
+ branchApi.setCurrentCompanyId(companyId); // Set company context
252
+ branchApi.fetchByCompany(companyId, search); // Fetch with company filter
178
253
  ```
179
254
 
180
255
  ### UserPermissionApiService
@@ -186,7 +261,7 @@ import { UserPermissionApiService } from '@flusys/ng-auth';
186
261
 
187
262
  const permApi = inject(UserPermissionApiService);
188
263
 
189
- // Company permissions
264
+ // Company Permissions
190
265
  permApi.assignUserCompanies({
191
266
  userId: 'user-1',
192
267
  items: [
@@ -194,27 +269,25 @@ permApi.assignUserCompanies({
194
269
  { targetId: 'company-2', isAdd: false } // Revoke
195
270
  ]
196
271
  });
197
- permApi.getUserCompanies(userId); // Get user's companies
272
+ permApi.getUserCompanies(userId); // POST /administration/permissions/get-user-company
198
273
  permApi.assignUserToCompany(userId, companyId); // Single assign
199
274
  permApi.revokeUserFromCompany(userId, companyId); // Single revoke
200
275
 
201
- // Branch permissions
276
+ // Branch Permissions
202
277
  permApi.assignUserBranches({
203
278
  userId: 'user-1',
204
- items: [
205
- { targetId: 'branch-1', isAdd: true }
206
- ]
279
+ items: [{ targetId: 'branch-1', isAdd: true }]
207
280
  });
208
- permApi.getUserBranches(userId); // Get user's branches
281
+ permApi.getUserBranches(userId, companyId?); // POST /administration/permissions/get-user-branch
209
282
  permApi.assignUserToBranch(userId, branchId);
210
283
  permApi.revokeUserFromBranch(userId, branchId);
211
284
  ```
212
285
 
213
286
  **Backend Endpoints:**
214
287
  - `POST /administration/permissions/user-company/assign`
215
- - `GET /administration/permissions/user-company?userId=xxx`
288
+ - `POST /administration/permissions/get-user-company`
216
289
  - `POST /administration/permissions/user-branch/assign`
217
- - `GET /administration/permissions/user-branch?userId=xxx`
290
+ - `POST /administration/permissions/get-user-branch`
218
291
 
219
292
  ---
220
293
 
@@ -235,13 +308,13 @@ import { authGuard } from '@flusys/ng-auth';
235
308
  ```
236
309
 
237
310
  **Checks:**
238
- 1. User is authenticated
311
+ 1. User is authenticated (`isAuthenticated()`)
239
312
  2. If company feature enabled: company AND branch are selected
240
- 3. Redirects to `/auth/login` with `returnUrl` if checks fail
313
+ 3. Redirects to `/auth/login` with `returnUrl` query param if checks fail
241
314
 
242
315
  ### guestGuard
243
316
 
244
- Protects auth routes (login, register) from authenticated users.
317
+ Protects auth routes (login, register) from authenticated users. Restores session first.
245
318
 
246
319
  ```typescript
247
320
  import { guestGuard } from '@flusys/ng-auth';
@@ -254,10 +327,11 @@ import { guestGuard } from '@flusys/ng-auth';
254
327
  ```
255
328
 
256
329
  **Behavior:**
257
- 1. Restores session via `AuthInitService.initialize()` first
258
- 2. If authenticated AND company selected: redirects to home or returnUrl
259
- 3. If selection pending: allows access (login handles Step 2)
260
- 4. If not authenticated: allows access
330
+ 1. If already initialized → check auth state immediately
331
+ 2. Otherwise → calls `AuthInitService.initialize()` first
332
+ 3. If authenticated AND company selected → redirects to home or returnUrl
333
+ 4. If selection pending → allows access (login handles Step 2)
334
+ 5. If not authenticated → allows access
261
335
 
262
336
  ### companyFeatureGuard
263
337
 
@@ -273,7 +347,39 @@ import { companyFeatureGuard } from '@flusys/ng-auth';
273
347
  }
274
348
  ```
275
349
 
276
- **Checks:** `services.auth.enabled` in APP_CONFIG. Redirects to `/` if disabled.
350
+ **Checks:** `enableCompanyFeature` in APP_CONFIG. Redirects to `/` if disabled.
351
+
352
+ ### emailFeatureGuard
353
+
354
+ Protects email-related routes (forgot-password, reset-password, verify-email).
355
+
356
+ ```typescript
357
+ import { emailFeatureGuard } from '@flusys/ng-auth';
358
+
359
+ {
360
+ path: 'auth/forgot-password',
361
+ canActivate: [emailFeatureGuard, guestGuard],
362
+ loadComponent: () => import('./forgot-password.component')
363
+ }
364
+ ```
365
+
366
+ **Checks:** `services.email.enabled` in APP_CONFIG. Redirects to `/auth/login` if disabled.
367
+
368
+ ### signUpFeatureGuard
369
+
370
+ Protects registration route when sign-up is disabled.
371
+
372
+ ```typescript
373
+ import { signUpFeatureGuard } from '@flusys/ng-auth';
374
+
375
+ {
376
+ path: 'auth/register',
377
+ canActivate: [signUpFeatureGuard, guestGuard],
378
+ loadComponent: () => import('./register.component')
379
+ }
380
+ ```
381
+
382
+ **Checks:** `enableSignUp` in APP_CONFIG (defaults to `true`). Redirects to `/auth/login` if `false`.
277
383
 
278
384
  ---
279
385
 
@@ -293,12 +399,16 @@ provideHttpClient(
293
399
 
294
400
  **Adds:**
295
401
  - `Authorization: Bearer {token}` if token exists
296
- - Tenant header if multi-tenant enabled
297
- - `withCredentials: true` for auth endpoints (cookie support)
402
+ - Tenant header (`X-Tenant-ID` by default) if multi-tenant enabled
403
+ - `withCredentials: true` + `cookie` header for auth endpoints (cookie support)
404
+
405
+ **Cookie Endpoints:**
406
+ - `/auth/login`, `/auth/logout`, `/auth/refresh`, `/auth/register`
407
+ - `/auth/select`, `/auth/switch-company`, `/auth/verify-email`
298
408
 
299
409
  ### tokenRefreshInterceptor
300
410
 
301
- Handles 401 responses and token refresh.
411
+ Handles 401 responses and proactive token refresh.
302
412
 
303
413
  ```typescript
304
414
  import { tokenRefreshInterceptor } from '@flusys/ng-auth';
@@ -309,11 +419,20 @@ provideHttpClient(
309
419
  ```
310
420
 
311
421
  **Behavior:**
312
- 1. Intercepts 401 Unauthorized responses
313
- 2. Skips refresh for: `/auth/refresh`, `/auth/login`, `/auth/register`, `/auth/select`
314
- 3. Uses `TokenRefreshStateService` to queue concurrent requests during refresh
315
- 4. On success: updates token, retries original request
316
- 5. On failure: logs out, shows error, redirects to login
422
+
423
+ 1. **Skip endpoints:** `/auth/refresh`, `/auth/login`, `/auth/select`, `/auth/register`
424
+
425
+ 2. **Proactive refresh:** Before request, checks if `isTokenExpired()` (expired or expiring within 30s)
426
+ - If expired → refresh token first, then retry with new token
427
+
428
+ 3. **Reactive refresh:** After 401 response
429
+ - If refresh in progress → queue request, wait for new token
430
+ - If not refreshing → start refresh, then retry
431
+
432
+ 4. **On refresh failure:**
433
+ - Shows toast "Session Expired"
434
+ - Calls `authState.resetState()`
435
+ - Navigates to login
317
436
 
318
437
  ---
319
438
 
@@ -327,26 +446,32 @@ Centralized endpoint definitions used by interceptors.
327
446
  import { AUTH_ENDPOINTS, isEndpoint } from '@flusys/ng-auth';
328
447
 
329
448
  // Endpoint constants
330
- AUTH_ENDPOINTS.LOGIN // '/auth/login'
331
- AUTH_ENDPOINTS.LOGOUT // '/auth/logout'
332
- AUTH_ENDPOINTS.REFRESH // '/auth/refresh'
333
- AUTH_ENDPOINTS.REGISTER // '/auth/register'
334
- AUTH_ENDPOINTS.SELECT // '/auth/select'
335
- AUTH_ENDPOINTS.SWITCH_COMPANY // '/auth/switch-company'
449
+ AUTH_ENDPOINTS.LOGIN // '/auth/login'
450
+ AUTH_ENDPOINTS.LOGOUT // '/auth/logout'
451
+ AUTH_ENDPOINTS.REFRESH // '/auth/refresh'
452
+ AUTH_ENDPOINTS.REGISTER // '/auth/register'
453
+ AUTH_ENDPOINTS.ME // '/auth/me'
454
+ AUTH_ENDPOINTS.SELECT // '/auth/select'
455
+ AUTH_ENDPOINTS.SWITCH_COMPANY // '/auth/switch-company'
456
+ AUTH_ENDPOINTS.CHANGE_PASSWORD // '/auth/change-password'
457
+ AUTH_ENDPOINTS.VERIFY_EMAIL // '/auth/verify-email'
458
+ AUTH_ENDPOINTS.FORGOT_PASSWORD // '/auth/forgot-password'
459
+ AUTH_ENDPOINTS.RESET_PASSWORD // '/auth/reset-password'
460
+ AUTH_ENDPOINTS.RESEND_VERIFICATION // '/auth/resend-verification'
336
461
 
337
462
  // Helper function
338
- isEndpoint(url, endpoint); // Check if URL matches specific endpoint
463
+ isEndpoint(url, endpoint); // Check if URL contains endpoint
339
464
  ```
340
465
 
341
466
  ---
342
467
 
343
468
  ## Provider Adapters
344
469
 
345
- ng-auth implements provider interfaces from ng-shared, enabling ng-iam and ng-storage to access auth data without direct dependencies.
470
+ ng-auth implements provider interfaces from ng-shared and ng-layout, enabling feature packages to access auth data without direct dependencies.
346
471
 
347
472
  ### provideAuthProviders()
348
473
 
349
- Registers provider adapters for ng-iam/ng-storage.
474
+ Registers provider adapters for ng-shared interfaces (used by ng-iam, ng-storage, ng-form-builder).
350
475
 
351
476
  ```typescript
352
477
  import { provideAuthProviders } from '@flusys/ng-auth';
@@ -361,6 +486,7 @@ providers: [
361
486
  - `USER_PROVIDER` → `AuthUserProviderAdapter` (user list for ng-iam)
362
487
  - `COMPANY_API_PROVIDER` → `AuthCompanyApiProviderAdapter` (company list for ng-iam)
363
488
  - `USER_PERMISSION_PROVIDER` → `AuthUserPermissionProviderAdapter` (permission queries)
489
+ - `AUTH_STATE_PROVIDER` → `AuthStateProviderAdapter` (isAuthenticated, initialize)
364
490
 
365
491
  ### provideAuthLayoutIntegration()
366
492
 
@@ -379,13 +505,20 @@ providers: [
379
505
  - `LAYOUT_AUTH_STATE` → `AuthLayoutStateAdapter` (user/company/branch signals)
380
506
  - `LAYOUT_AUTH_API` → `AuthLayoutApiAdapter` (logout, switch company, etc.)
381
507
 
508
+ **AuthLayoutStateAdapter Signals:**
509
+ ```typescript
510
+ currentCompanyInfo: Signal<ICompanyInfo | null>; // { id, name, slug, imageId }
511
+ currentBranchInfo: Signal<IBranchInfo | null>; // { id, name, slug, imageId }
512
+ loginUserData: Signal<ILayoutUserInfo | null>; // { id, name, email }
513
+ ```
514
+
382
515
  **AuthLayoutApiAdapter Methods:**
383
516
  ```typescript
384
- logOut();
385
- navigateLogin(withUrl?: boolean);
386
- switchCompany(companyId, branchId); // Triggers page reload
387
- getUserCompanies(); // User's permitted companies
388
- getCompanyBranches(companyId); // User's permitted branches
517
+ logOut(); // Logout + reset state
518
+ navigateLogin(withUrl?: boolean); // Navigate to login, optionally with returnUrl
519
+ switchCompany(companyId, branchId); // Switch + page reload
520
+ getUserCompanies(); // User's permitted companies
521
+ getCompanyBranches(companyId); // User's permitted branches in company
389
522
  ```
390
523
 
391
524
  ---
@@ -398,47 +531,66 @@ All page components follow Angular 21 best practices:
398
531
  - 100% responsive design (mobile-first with Tailwind)
399
532
  - Dark mode compatible (PrimeNG + Tailwind CSS variables)
400
533
  - `inject()` for dependency injection
534
+ - `effect()` for reactive data loading
401
535
 
402
536
  ### Authentication Pages
403
537
 
404
538
  **LoginPageComponent** (`/auth/login`)
405
- - Multi-step login: Step 1 = credentials, Step 2 = company/branch selection (if needed)
406
- - Visual step indicator (when company feature enabled)
407
- - Email verification step (when email not verified)
539
+ - Multi-step login flow with visual step indicator (when company feature enabled)
540
+ - Step 1: Credentials (email/password)
541
+ - Step 2: Company/Branch selection (if `requiresSelection: true`)
542
+ - Email verification step (if `requiresEmailVerification: true`)
408
543
  - Auto-selects if only one company/branch available
544
+ - Restores stored company/branch from localStorage
409
545
  - Stores returnUrl for post-login redirect
410
- - Responsive form layout with mobile padding
546
+ - Support for AUTH_VALIDATION_CONFIG (custom email/password validation)
547
+ - **Conditional UI:**
548
+ - "Forgot password?" link hidden when `services.email.enabled: false`
549
+ - "Sign Up" link hidden when `enableSignUp: false`
411
550
 
412
551
  **RegisterPageComponent** (`/auth/register`)
413
552
  - User registration with optional company creation
414
553
  - Two-column responsive grid layout
415
- - Email/password validation with signal forms
554
+ - Email/password validation with AUTH_VALIDATION_CONFIG support
416
555
  - Company details section (when company feature enabled)
556
+ - Support for AUTH_REGISTRATION_EXTENSION (extra fields)
557
+ - **Default company/branch support:**
558
+ - When `defaultCompanySlug` is set: pre-fills company code field (readonly)
559
+ - When `defaultBranchSlug` is set: pre-fills branch code field (readonly)
560
+ - "Create new company" option hidden when defaults are configured
561
+ - Query param support: `?companySlug=xxx`
417
562
 
418
563
  **ForgotPasswordPageComponent** (`/auth/forgot-password`)
419
564
  - Email input with validation
420
565
  - Success state showing email sent confirmation
566
+ - Always shows success (security: don't reveal if email exists)
421
567
 
422
568
  **ResetPasswordPageComponent** (`/auth/reset-password`)
423
- - Password reset with token validation
569
+ - Password reset with token from query param `?token=xxx`
424
570
  - Password confirmation matching
425
- - Success/invalid token states
571
+ - Three states: `form`, `success`, `invalid-token`
572
+ - Auto-detects expired/invalid token from error message
426
573
 
427
574
  **VerifyEmailPageComponent** (`/auth/verify-email`)
428
- - Token verification with SSR support (TransferState)
429
- - Resend verification option
430
- - Success/failure states
575
+ - Token verification from query param `?token=xxx`
576
+ - SSR support via TransferState (prevents duplicate API calls)
577
+ - Four states: `verifying`, `success`, `invalid-token`, `resend`
578
+ - Resend verification form when token invalid/expired
431
579
 
432
580
  ### Profile Page
433
581
 
434
582
  **ProfilePageComponent** (`/profile`)
435
583
  - Page title with description
436
584
  - Avatar section with initials fallback
437
- - Update name and phone
438
- - Upload profile picture (requires `PROFILE_UPLOAD_PROVIDER` from ng-storage)
439
- - Change password section with strength indicator
440
- - View company/branch info (if company feature enabled)
441
- - View roles/actions (requires `PROFILE_PERMISSION_PROVIDER` from ng-iam)
585
+ - Profile picture upload (requires `PROFILE_UPLOAD_PROVIDER` from ng-storage)
586
+ - Update name and phone with Signal Forms
587
+ - Extra fields support via AUTH_PROFILE_EXTENSION
588
+ - Editable fields (user can modify)
589
+ - View-only fields (managed by admin)
590
+ - Change password section with PrimeNG password strength indicator
591
+ - Company/branch info (if company feature enabled)
592
+ - Roles/actions display (requires `PROFILE_PERMISSION_PROVIDER` from ng-iam)
593
+ - Permission mode support: `rbac`, `direct`, `full`
442
594
  - Responsive two-column layout on desktop
443
595
 
444
596
  **Optional Provider Dependencies:**
@@ -448,30 +600,31 @@ All page components follow Angular 21 best practices:
448
600
  | `PROFILE_UPLOAD_PROVIDER` | ng-storage via `provideStorageProviders()` | Profile picture upload |
449
601
  | `PROFILE_PERMISSION_PROVIDER` | ng-iam via `provideIamProviders()` | Roles/actions display |
450
602
 
451
- **Graceful Degradation:**
452
- - If provider not registered, feature shows "not enabled" message
453
- - If service disabled in config, section is hidden
454
- - No errors thrown for missing optional providers
455
-
456
603
  ### Administration Pages
457
604
 
458
605
  **AdministrationPageComponent** (`/administration`)
459
606
  - Page title "Administration" with dynamic subtitle
460
- - Horizontal scrolling tabs (hidden scrollbar on mobile)
607
+ - Horizontal scrolling tabs with hidden scrollbar
461
608
  - Tabs: Users, Companies, Branches
462
609
  - Company/Branch tabs only visible when company feature enabled
610
+ - Permission-based tab visibility (USER_PERMISSIONS.READ, etc.)
463
611
 
464
612
  **User Management:**
465
- - `UserListComponent` - LazyLoad datatable with Edit, Delete, Permission actions
466
- - `UserFormComponent` - Create/edit user with auto company/branch assignment
467
- - Permission dialogs for company/branch assignments
613
+ - `UserListComponent` - LazyLoad datatable with Edit, Delete, View Details, Permission actions
614
+ - Extra columns/actions support via AUTH_USER_LIST_EXTENSION
615
+ - User detail view via AUTH_USER_DETAIL_CONFIG (dialog or page mode)
616
+ - `UserFormComponent` - Create/edit user
617
+ - Auto-assigns new users to current company/branch when company feature enabled
468
618
 
469
619
  **Company Management:**
470
620
  - `CompanyListComponent` - Company CRUD with View Branches action
621
+ - readOnly companies cannot be deleted
471
622
  - `CompanyFormComponent` - Create/edit company with auto slug generation
472
623
 
473
624
  **Branch Management:**
474
625
  - `BranchListComponent` - Branch CRUD filtered by company dropdown
626
+ - Defaults to current company context
627
+ - Supports `?companyId=xxx` query param
475
628
  - `BranchFormComponent` - Create/edit branch with company selection
476
629
 
477
630
  **Table Features (all list components):**
@@ -550,13 +703,10 @@ import { UserBranchPermissionDialogComponent } from '@flusys/ng-auth';
550
703
  - Company dropdown shows only user's permitted companies
551
704
  - Branch list loads on company selection
552
705
  - Immediate save on toggle
553
- - Contextual empty messages (no permissions vs no branches)
554
- - Scoped to user's permitted companies
555
-
556
- **Workflow:**
557
- 1. Dialog opens → Loads user's permitted companies via effect()
558
- 2. User selects company → Loads branches for that company
559
- 3. User toggles branch checkboxes → Immediately assigns/revokes
706
+ - Contextual empty messages:
707
+ - "This user has no company permissions assigned" (no companies)
708
+ - "Please select a company to view branches" (no selection)
709
+ - "No branches available" (empty)
560
710
 
561
711
  ---
562
712
 
@@ -569,25 +719,46 @@ Public authentication routes.
569
719
  ```typescript
570
720
  import { AUTH_ROUTES } from '@flusys/ng-auth';
571
721
 
572
- // Routes: /auth/login, /auth/register
722
+ // Routes: /auth/login, /auth/register, /auth/forgot-password, /auth/reset-password, /auth/verify-email
573
723
  { path: 'auth', children: AUTH_ROUTES }
574
724
  ```
575
725
 
726
+ **Route Configuration:**
727
+ | Path | Component | Guards |
728
+ |------|-----------|--------|
729
+ | `/auth/login` | LoginPageComponent | guestGuard |
730
+ | `/auth/register` | RegisterPageComponent | signUpFeatureGuard, guestGuard |
731
+ | `/auth/forgot-password` | ForgotPasswordPageComponent | emailFeatureGuard, guestGuard |
732
+ | `/auth/reset-password` | ResetPasswordPageComponent | emailFeatureGuard, guestGuard |
733
+ | `/auth/verify-email` | VerifyEmailPageComponent | emailFeatureGuard |
734
+
576
735
  ### ADMINISTRATION_ROUTES
577
736
 
578
- Protected administration routes.
737
+ Protected administration routes with tabbed interface.
579
738
 
580
739
  ```typescript
581
740
  import { ADMINISTRATION_ROUTES } from '@flusys/ng-auth';
582
741
 
583
- // Routes: /administration/users/*, /administration/company/*, /administration/branch/*
584
742
  {
585
743
  path: 'administration',
586
- canActivate: [authGuard],
587
744
  children: ADMINISTRATION_ROUTES
588
745
  }
589
746
  ```
590
747
 
748
+ **Route Configuration:**
749
+ | Path | Component | Guards |
750
+ |------|-----------|--------|
751
+ | `/administration` | AdministrationPageComponent | authGuard |
752
+ | `/administration/users` | UserListComponent | permissionGuard(USER_PERMISSIONS.READ) |
753
+ | `/administration/users/new` | UserFormComponent | |
754
+ | `/administration/users/:id` | UserFormComponent | |
755
+ | `/administration/company` | CompanyListComponent | companyFeatureGuard, permissionGuard(COMPANY_PERMISSIONS.READ) |
756
+ | `/administration/company/new` | CompanyFormComponent | |
757
+ | `/administration/company/:id` | CompanyFormComponent | |
758
+ | `/administration/branch` | BranchListComponent | companyFeatureGuard, permissionGuard(BRANCH_PERMISSIONS.READ) |
759
+ | `/administration/branch/new` | BranchFormComponent | |
760
+ | `/administration/branch/:id` | BranchFormComponent | |
761
+
591
762
  ### PROFILE_ROUTES
592
763
 
593
764
  User profile page routes.
@@ -595,10 +766,8 @@ User profile page routes.
595
766
  ```typescript
596
767
  import { PROFILE_ROUTES } from '@flusys/ng-auth';
597
768
 
598
- // Route: /profile
599
769
  {
600
770
  path: 'profile',
601
- canActivate: [authGuard],
602
771
  children: PROFILE_ROUTES
603
772
  }
604
773
  ```
@@ -627,15 +796,15 @@ export const appConfig: ApplicationConfig = {
627
796
  provideHttpClient(
628
797
  withFetch(),
629
798
  withInterceptors([
630
- authInterceptor,
631
- tokenRefreshInterceptor,
632
- errorCatchingInterceptor
799
+ authInterceptor, // First: adds token
800
+ tokenRefreshInterceptor, // Second: handles 401 + proactive refresh
801
+ errorCatchingInterceptor // Last: shows errors
633
802
  ])
634
803
  ),
635
804
  { provide: APP_CONFIG, useValue: DEFAULT_APP_CONFIG },
636
805
  ...provideAuthLayoutIntegration(),
637
806
  ...provideAuthProviders(),
638
- ...provideIamProviders(), // Enables permissions display on profile page
807
+ ...provideIamProviders(), // Enables permissions display on profile
639
808
  ...provideStorageProviders(), // Enables profile picture upload
640
809
  ]
641
810
  };
@@ -645,7 +814,7 @@ export const appConfig: ApplicationConfig = {
645
814
 
646
815
  ```typescript
647
816
  import { Routes } from '@angular/router';
648
- import { authGuard, guestGuard, AUTH_ROUTES, ADMINISTRATION_ROUTES, PROFILE_ROUTES } from '@flusys/ng-auth';
817
+ import { authGuard, AUTH_ROUTES, ADMINISTRATION_ROUTES, PROFILE_ROUTES } from '@flusys/ng-auth';
649
818
 
650
819
  export const routes: Routes = [
651
820
  { path: 'auth', children: AUTH_ROUTES },
@@ -668,34 +837,175 @@ export const routes: Routes = [
668
837
  ### Request DTOs
669
838
 
670
839
  ```typescript
671
- interface ILoginRequest { email: string; password: string; }
672
- interface IRegistrationRequest { name: string; email: string; password: string; phone?: string; }
673
- interface ISelectRequest { companyId: string; branchId?: string; sessionId: string; }
674
- interface ISwitchCompanyRequest { companyId: string; branchId: string; }
675
- interface IChangePasswordRequest { currentPassword: string; newPassword: string; }
840
+ interface ILoginRequest {
841
+ email: string;
842
+ password: string;
843
+ }
844
+
845
+ interface IRegistrationRequest {
846
+ name: string;
847
+ email: string;
848
+ password: string;
849
+ phone?: string;
850
+ companySlug?: string;
851
+ branchSlug?: string;
852
+ newCompanyName?: string;
853
+ newCompanyPhone?: string;
854
+ newCompanyAddress?: string;
855
+ additionalFields?: Record<string, unknown>;
856
+ }
857
+
858
+ interface ISelectRequest {
859
+ sessionId: string;
860
+ companyId: string;
861
+ branchId: string;
862
+ }
863
+
864
+ interface ISwitchCompanyRequest {
865
+ companyId: string;
866
+ branchId: string;
867
+ }
868
+
869
+ interface IChangePasswordRequest {
870
+ currentPassword: string;
871
+ newPassword: string;
872
+ }
873
+
874
+ interface ICreateUserRequest {
875
+ name: string;
876
+ email: string;
877
+ password: string;
878
+ phone?: string;
879
+ profilePictureId?: string;
880
+ isActive?: boolean;
881
+ emailVerified?: boolean;
882
+ phoneVerified?: boolean;
883
+ additionalFields?: Record<string, unknown>;
884
+ }
885
+
886
+ interface IUpdateUserRequest {
887
+ id: string;
888
+ name?: string;
889
+ email?: string;
890
+ password?: string;
891
+ oldPassword?: string;
892
+ phone?: string;
893
+ profilePictureId?: string;
894
+ isActive?: boolean;
895
+ emailVerified?: boolean;
896
+ phoneVerified?: boolean;
897
+ additionalFields?: Record<string, unknown>;
898
+ }
899
+
900
+ interface ICreateCompanyRequest {
901
+ name: string;
902
+ slug: string;
903
+ logoId?: string;
904
+ address?: string;
905
+ phone?: string;
906
+ email?: string;
907
+ website?: string;
908
+ serial?: number;
909
+ isActive?: boolean;
910
+ settings?: Record<string, unknown>;
911
+ additionalFields?: Record<string, unknown>;
912
+ }
913
+
914
+ interface ICreateBranchRequest {
915
+ name: string;
916
+ slug: string;
917
+ companyId: string;
918
+ parentId?: string;
919
+ logoId?: string;
920
+ address?: string;
921
+ phone?: string;
922
+ email?: string;
923
+ serial?: number;
924
+ isActive?: boolean;
925
+ additionalFields?: Record<string, unknown>;
926
+ }
676
927
  ```
677
928
 
678
929
  ### Response DTOs
679
930
 
680
931
  ```typescript
932
+ interface IUserInfo {
933
+ id: string;
934
+ email: string;
935
+ name?: string;
936
+ phone?: string | null;
937
+ profilePictureId?: string | null;
938
+ companyId?: string;
939
+ branchId?: string;
940
+ companyLogoId?: string;
941
+ branchLogoId?: string;
942
+ }
943
+
944
+ interface ICompanyInfo {
945
+ id: string;
946
+ name: string;
947
+ slug?: string;
948
+ logoId?: string | null;
949
+ }
950
+
951
+ interface IBranchInfo {
952
+ id: string;
953
+ name: string;
954
+ slug?: string;
955
+ logoId?: string | null;
956
+ }
957
+
958
+ interface ICompanyWithBranches {
959
+ id: string;
960
+ name: string;
961
+ slug: string;
962
+ logoId?: string | null;
963
+ branches: IBranchSelectionInfo[];
964
+ }
965
+
681
966
  interface ILoginResponse {
682
967
  accessToken: string;
968
+ refreshToken: string;
683
969
  user: IUserInfo;
684
- requiresSelection?: boolean;
685
- sessionId?: string;
686
- companies?: ICompanyWithBranches[];
687
- expiresAt?: number;
970
+ company?: ICompanyInfo;
971
+ branch?: IBranchInfo;
688
972
  }
689
973
 
690
974
  interface ICompanySelectionResponse {
691
- accessToken: string;
975
+ requiresSelection: true;
976
+ sessionId: string;
977
+ expiresAt: Date | string;
692
978
  user: IUserInfo;
693
- company: ICompanyInfo;
694
- branch: IBranchInfo;
979
+ companies: ICompanyWithBranches[];
695
980
  }
696
981
 
697
- interface IMeResponse extends IUserInfo {}
698
- interface IRefreshTokenResponse { accessToken: string; }
982
+ interface ILoginRequiresEmailVerification {
983
+ requiresEmailVerification: true;
984
+ email: string;
985
+ message: string;
986
+ }
987
+
988
+ interface IMeResponse {
989
+ user: IUserInfo;
990
+ company?: ICompanyInfo;
991
+ branch?: IBranchInfo;
992
+ }
993
+
994
+ interface IRegistrationResponse {
995
+ accessToken?: string;
996
+ refreshToken?: string;
997
+ user: IUserInfo;
998
+ company?: ICompanyInfo;
999
+ branch?: IBranchInfo;
1000
+ requiresEmailVerification?: boolean;
1001
+ }
1002
+
1003
+ interface IRefreshTokenResponse {
1004
+ accessToken: string;
1005
+ refreshToken?: string;
1006
+ }
1007
+
1008
+ type AuthResponse = ILoginResponse | ICompanySelectionResponse | ILoginRequiresEmailVerification;
699
1009
  ```
700
1010
 
701
1011
  ### Entity DTOs
@@ -705,28 +1015,34 @@ interface IUser {
705
1015
  id: string;
706
1016
  name: string;
707
1017
  email: string;
708
- phone?: string;
709
- profilePictureId?: string;
1018
+ phone?: string | null;
1019
+ profilePictureId?: string | null;
710
1020
  isActive: boolean;
711
- isEmailVerified: boolean;
712
- isPhoneVerified: boolean;
713
- lastLoginAt?: Date;
714
- createdAt: Date;
715
- updatedAt: Date;
1021
+ emailVerified: boolean;
1022
+ phoneVerified: boolean;
1023
+ lastLoginAt?: Date | string | null;
1024
+ additionalFields?: Record<string, unknown> | null;
1025
+ createdAt: Date | string;
1026
+ updatedAt: Date | string;
1027
+ deletedAt?: Date | string | null;
716
1028
  }
717
1029
 
718
1030
  interface ICompany {
719
1031
  id: string;
720
1032
  name: string;
721
1033
  slug: string;
722
- logoId?: string;
723
- address?: string;
724
- phone?: string;
725
- email?: string;
726
- website?: string;
1034
+ logoId?: string | null;
1035
+ address?: string | null;
1036
+ phone?: string | null;
1037
+ email?: string | null;
1038
+ website?: string | null;
1039
+ serial?: number;
727
1040
  isActive: boolean;
728
- createdAt: Date;
729
- updatedAt: Date;
1041
+ readOnly: boolean;
1042
+ settings?: Record<string, unknown> | null;
1043
+ additionalFields?: Record<string, unknown> | null;
1044
+ createdAt: Date | string;
1045
+ updatedAt: Date | string;
730
1046
  }
731
1047
 
732
1048
  interface IBranch {
@@ -734,26 +1050,71 @@ interface IBranch {
734
1050
  name: string;
735
1051
  slug: string;
736
1052
  companyId: string;
737
- parentId?: string;
738
- logoId?: string;
739
- address?: string;
740
- phone?: string;
741
- email?: string;
1053
+ parentId?: string | null;
1054
+ logoId?: string | null;
1055
+ address?: string | null;
1056
+ phone?: string | null;
1057
+ email?: string | null;
1058
+ serial?: number;
742
1059
  isActive: boolean;
743
- createdAt: Date;
744
- updatedAt: Date;
1060
+ readOnly: boolean;
1061
+ additionalFields?: Record<string, unknown> | null;
1062
+ createdAt: Date | string;
1063
+ updatedAt: Date | string;
745
1064
  }
746
1065
  ```
747
1066
 
748
1067
  ### Permission DTOs
749
1068
 
750
1069
  ```typescript
751
- interface IPermissionItem { targetId: string; isAdd: boolean; }
752
- interface IAssignUserCompanyRequest { userId: string; items: IPermissionItem[]; }
753
- interface IAssignUserBranchRequest { userId: string; items: IPermissionItem[]; }
754
- interface IBatchOperationResponse { success: boolean; message: string; }
755
- interface IUserCompanyPermission { id: string; userId: string; companyId: string; companyName: string; companySlug: string; }
756
- interface IUserBranchPermission { id: string; userId: string; branchId: string; branchName: string; branchSlug: string; companyId: string; }
1070
+ interface IPermissionItem {
1071
+ targetId: string;
1072
+ isAdd: boolean;
1073
+ }
1074
+
1075
+ interface IAssignUserCompanyRequest {
1076
+ userId: string;
1077
+ items: IPermissionItem[];
1078
+ }
1079
+
1080
+ interface IAssignUserBranchRequest {
1081
+ userId: string;
1082
+ items: IPermissionItem[];
1083
+ }
1084
+
1085
+ interface IBatchOperationResponse {
1086
+ success: boolean;
1087
+ message: string;
1088
+ }
1089
+
1090
+ interface IUserCompanyPermission {
1091
+ id: string;
1092
+ userId: string;
1093
+ companyId: string;
1094
+ companyName: string;
1095
+ companySlug: string;
1096
+ }
1097
+
1098
+ interface IUserBranchPermission {
1099
+ id: string;
1100
+ userId: string;
1101
+ branchId: string;
1102
+ branchName: string;
1103
+ branchSlug: string;
1104
+ companyId: string;
1105
+ }
1106
+
1107
+ interface IUserCompaniesResponse {
1108
+ success: boolean;
1109
+ message: string;
1110
+ data: IUserCompanyPermission[];
1111
+ }
1112
+
1113
+ interface IUserBranchesResponse {
1114
+ success: boolean;
1115
+ message: string;
1116
+ data: IUserBranchPermission[];
1117
+ }
757
1118
  ```
758
1119
 
759
1120
  ---
@@ -762,44 +1123,18 @@ interface IUserBranchPermission { id: string; userId: string; branchId: string;
762
1123
 
763
1124
  ### Angular 21 Component Patterns
764
1125
 
765
- All components in ng-auth follow these patterns:
1126
+ All components follow these patterns:
766
1127
 
767
- ```typescript
768
- @Component({
769
- selector: 'lib-example',
770
- standalone: true,
771
- changeDetection: ChangeDetectionStrategy.OnPush, // Always OnPush
772
- imports: [AngularModule, PrimeModule, FormField],
773
- template: `...`
774
- })
775
- export class ExampleComponent {
776
- // Dependency injection via inject()
777
- private readonly service = inject(MyService);
778
-
779
- // Signal-based state
780
- readonly isLoading = signal(false);
781
- readonly data = signal<IData | null>(null);
782
-
783
- // Computed signals
784
- readonly isValid = computed(() => this.data() !== null);
785
-
786
- // Signal Forms
787
- readonly formModel = signal<IFormModel>({ name: '', email: '' });
788
- readonly formTree = form(this.formModel, (f) => {
789
- required(f.name, { message: 'Name is required' });
790
- });
791
-
792
- // Error handling with proper typing
793
- async onSubmit(): Promise<void> {
794
- try {
795
- // ...
796
- } catch (error: unknown) {
797
- const err = error as { error?: { message?: string } };
798
- // Handle error
799
- }
800
- }
801
- }
802
- ```
1128
+ | Pattern | Implementation |
1129
+ |---------|---------------|
1130
+ | Change Detection | `ChangeDetectionStrategy.OnPush` |
1131
+ | Dependency Injection | `inject()` function |
1132
+ | State (private writable) | `private readonly _data = signal<T>(...)` |
1133
+ | State (public readonly) | `readonly data = this._data.asReadonly()` |
1134
+ | Computed | `readonly isValid = computed(() => ...)` |
1135
+ | Signal Forms | `form(this.formModel, (f) => { required(f.field, {...}) })` |
1136
+ | Reactive Init | `effect(() => { if (condition()) loadData(); })` |
1137
+ | Async Methods | `try/finally` with `isLoading` signal, errors handled by global interceptor |
803
1138
 
804
1139
  ### Responsive Design Patterns
805
1140
 
@@ -842,7 +1177,11 @@ projects/ng-auth/
842
1177
  ├── services/
843
1178
  ├── guards/
844
1179
  ├── interceptors/
845
- └── adapters/
1180
+ ├── adapters/
1181
+ ├── providers/
1182
+ ├── interfaces/
1183
+ ├── constants/
1184
+ └── routes/
846
1185
  ```
847
1186
 
848
1187
  **Rule:** Dialog components go in `components/`, page components go in `pages/`.
@@ -862,9 +1201,13 @@ const allCompanies = await companyApi.fetchList(); // Security risk
862
1201
 
863
1202
  ### Token Management
864
1203
 
865
- - Use httpOnly cookies for refresh tokens (backend)
866
- - Access tokens stored in memory via AuthStateService
867
- - TokenRefreshInterceptor handles automatic refresh
1204
+ - Access tokens stored in memory only (XSS protection)
1205
+ - Only token expiry is persisted to localStorage
1206
+ - Refresh tokens sent via httpOnly cookies
1207
+ - TokenRefreshInterceptor handles:
1208
+ - Proactive refresh (30s before expiry)
1209
+ - Reactive refresh (on 401)
1210
+ - Request queuing during refresh
868
1211
  - Auto-persistence for company/branch selection
869
1212
 
870
1213
  ### Type Safety
@@ -874,211 +1217,196 @@ Avoid `any` types - use explicit interfaces:
874
1217
  ```typescript
875
1218
  // ✅ CORRECT - Explicit response type
876
1219
  .subscribe({
877
- next: (result: { message?: string }) => {
878
- this.messageService.add({
879
- severity: 'success',
880
- detail: result.message,
881
- });
1220
+ next: (response: { success: boolean; data: IUser }) => {
1221
+ if (response.success) { ... }
882
1222
  },
883
1223
  });
884
1224
 
885
1225
  // ❌ WRONG - Using any
886
1226
  .subscribe({
887
- next: (result: any) => { ... },
1227
+ next: (response: any) => { ... },
888
1228
  });
889
1229
  ```
890
1230
 
891
1231
  ### Multi-Company Flow
892
1232
 
893
- 1. Login → Check if `requiresSelection: true`
894
- 2. If yes → Show Step 2 (company/branch selection)
895
- 3. Auto-select if only one company/branch
896
- 4. Save selection → Navigate to dashboard
1233
+ 1. Login → Check response type with type guards
1234
+ 2. If `isCompanySelectionResponse` → Show Step 2 (company/branch selection)
1235
+ 3. If `isEmailVerificationRequired` → Show email verification step
1236
+ 4. If `isLoginResponse` → Direct login success
1237
+ 5. Auto-select if only one company/branch
1238
+ 6. Save selection → Navigate to dashboard
897
1239
 
898
1240
  ---
899
1241
 
900
1242
  ## Common Issues
901
1243
 
902
- ### Guards Not Working
1244
+ | Problem | Solution |
1245
+ |---------|----------|
1246
+ | Routes accessible without auth | Ensure `authGuard` is on parent route |
1247
+ | 401 on all requests | Ensure interceptor order: `authInterceptor` → `tokenRefreshInterceptor` → `errorCatchingInterceptor` |
1248
+ | User info missing from topbar | Add `...provideAuthLayoutIntegration()` to providers |
1249
+ | Switcher shows all companies | Ensure user has company permissions assigned via `UserPermissionApiService` |
1250
+ | Session not restored on refresh | 1) Use `guestGuard` on auth routes 2) Check refresh cookie in DevTools 3) Verify backend refresh endpoint |
903
1251
 
904
- **Problem:** Routes accessible without authentication.
1252
+ ---
905
1253
 
906
- **Solution:** Ensure authGuard is on parent route and session is restored:
907
- ```typescript
908
- {
909
- path: '',
910
- canActivate: [authGuard], // Parent guard
911
- children: [...]
912
- }
913
- ```
1254
+ ## No-IAM Mode (Bypassing Permission Guards)
914
1255
 
915
- ### Token Not Sent
1256
+ When not using `@flusys/ng-iam`, routes with `permissionGuard` will fail (no permissions loaded).
916
1257
 
917
- **Problem:** 401 on all requests.
1258
+ **Solution:** Set wildcard permissions after login:
918
1259
 
919
- **Solution:** Ensure interceptor order is correct:
920
1260
  ```typescript
921
- withInterceptors([
922
- authInterceptor, // First: adds token
923
- tokenRefreshInterceptor, // Second: handles 401
924
- errorCatchingInterceptor // Last: shows errors
925
- ])
926
- ```
1261
+ // Option 1: In login flow (recommended)
1262
+ this.permissionValidator.setPermissions(['*']);
927
1263
 
928
- ### Layout Not Showing User
1264
+ // Option 2: In APP_INITIALIZER
1265
+ { provide: APP_INITIALIZER, useFactory: (pv: PermissionValidatorService) => () => pv.setPermissions(['*']), deps: [PermissionValidatorService], multi: true }
1266
+ ```
929
1267
 
930
- **Problem:** User info missing from topbar.
1268
+ | Pattern | Meaning |
1269
+ |---------|---------|
1270
+ | `*` | Full access to everything |
1271
+ | `user.*` | All user permissions |
1272
+ | `branch.*` | All branch permissions |
931
1273
 
932
- **Solution:** Ensure layout integration is provided:
933
- ```typescript
934
- providers: [
935
- ...provideAuthLayoutIntegration()
936
- ]
937
- ```
1274
+ ---
938
1275
 
939
- ### Company Switcher Shows All Companies
1276
+ ## Extension System
940
1277
 
941
- **Problem:** Switcher shows companies user doesn't have access to.
1278
+ ng-auth provides injection tokens for extending registration, user list, and profile without modifying the core package.
942
1279
 
943
- **Solution:** `AuthLayoutApiAdapter.getUserCompanies()` uses `UserPermissionApiService.getUserCompanies(userId)` internally - ensure user has company permissions assigned.
1280
+ ### Extension Injection Tokens
944
1281
 
945
- ---
1282
+ ```typescript
1283
+ import {
1284
+ AUTH_VALIDATION_CONFIG,
1285
+ AUTH_REGISTRATION_EXTENSION,
1286
+ AUTH_USER_LIST_EXTENSION,
1287
+ AUTH_USER_DETAIL_CONFIG,
1288
+ AUTH_PROFILE_EXTENSION,
1289
+ } from '@flusys/ng-auth';
1290
+ ```
946
1291
 
947
- ## No-IAM Mode (Bypassing Permission Guards)
1292
+ ### AUTH_VALIDATION_CONFIG
948
1293
 
949
- When you don't use the `@flusys/ng-iam` module but your routes use `permissionGuard`, the guard will fail because no permissions are loaded.
1294
+ Customize email and password validation.
950
1295
 
951
- **Solution:** Set wildcard permissions in `PermissionValidatorService` after login.
1296
+ | Property | Options |
1297
+ |----------|---------|
1298
+ | `email.pattern` | Custom regex |
1299
+ | `email.message` | Error message |
1300
+ | `password.minLength/maxLength` | Length constraints |
1301
+ | `password.requireUppercase/Lowercase/Numbers/SpecialChars` | Character requirements |
1302
+ | `password.pattern` | Custom regex |
1303
+ | `password.message` | Error message |
952
1304
 
953
- ### How Permission Guard Works
1305
+ ```typescript
1306
+ { provide: AUTH_VALIDATION_CONFIG, useValue: { password: { minLength: 8, requireUppercase: true } } }
954
1307
 
955
- ```
956
- Route with permissionGuard('user.view')
957
- ↓
958
- permissionGuard() → inject(PermissionValidatorService)
959
- ↓
960
- permissionValidator.isPermissionsLoaded() → false
961
- ↓
962
- Redirect to '/' ❌
1308
+ // Helper functions
1309
+ import { validatePassword, validateEmail } from '@flusys/ng-auth';
963
1310
  ```
964
1311
 
965
- ### Fix: Set Wildcard Permissions After Login
1312
+ ### AUTH_REGISTRATION_EXTENSION
966
1313
 
967
- #### Option 1: In Login Flow (Recommended)
1314
+ Add extra fields to registration form.
968
1315
 
969
- Update your login handling to set permissions after successful authentication:
1316
+ **IRegistrationExtraField:** `key`, `label`, `type` (text|number|select|checkbox|textarea|date|template), `placeholder?`, `required?`, `defaultValue?`, `options?`, `pattern?`, `validationMessage?`, `order?`, `containerClass?`, `template?`, `component?`
1317
+
1318
+ **IRegistrationExtensionProvider Methods:**
1319
+
1320
+ | Method | Purpose |
1321
+ |--------|---------|
1322
+ | `getExtraFields()` | Return field definitions |
1323
+ | `transformRegistrationData?(data)` | Transform before submission (e.g., wrap in `additionalFields`) |
1324
+ | `validateExtraFields?(data)` | Custom validation, return error message or null |
970
1325
 
971
1326
  ```typescript
972
- // login.component.ts or auth.service.ts
973
- import { PermissionValidatorService } from '@flusys/ng-shared';
1327
+ { provide: AUTH_REGISTRATION_EXTENSION, useClass: MyExtension }
1328
+ // Custom component: inject(REGISTRATION_FIELD_CONTEXT) for field, value, updateValue()
1329
+ ```
974
1330
 
975
- @Component({ ... })
976
- export class LoginComponent {
977
- private readonly permissionValidator = inject(PermissionValidatorService);
978
- private readonly authApi = inject(AuthApiService);
979
- private readonly authState = inject(AuthStateService);
1331
+ ### AUTH_USER_LIST_EXTENSION
980
1332
 
981
- async onLogin(): Promise<void> {
982
- const response = await firstValueFrom(this.authApi.login(credentials));
983
- this.authState.setLoginState(response);
1333
+ Add extra columns and actions to user list.
984
1334
 
985
- // When IAM is NOT used, grant all permissions via wildcard
986
- this.permissionValidator.setPermissions(['*']);
1335
+ **IUserListExtraColumn:** `key`, `header`, `field?` (dot notation: `additionalFields.employeeId`), `template?`, `styleClass?`, `width?`, `sortable?`, `order?`
987
1336
 
988
- // Navigate to dashboard
989
- this.router.navigate(['/dashboard']);
990
- }
991
- }
992
- ```
1337
+ **IUserListExtraAction:** `key`, `label`, `icon`, `severity?`, `permission?`, `onClick(user)`, `isVisible?(user)`, `isDisabled?(user)`, `order?`
993
1338
 
994
- #### Option 2: In APP_INITIALIZER
1339
+ **IUserListExtensionProvider Methods:**
995
1340
 
996
- Set permissions at app startup:
1341
+ | Method | Purpose |
1342
+ |--------|---------|
1343
+ | `getExtraColumns()` | Return column definitions |
1344
+ | `getExtraActions()` | Return action button definitions |
1345
+ | `enrichUserData?(users)` | Transform user data (usually handled by backend enricher) |
997
1346
 
998
1347
  ```typescript
999
- // app.config.ts
1000
- import { APP_INITIALIZER } from '@angular/core';
1001
- import { PermissionValidatorService } from '@flusys/ng-shared';
1002
-
1003
- export const appConfig: ApplicationConfig = {
1004
- providers: [
1005
- {
1006
- provide: APP_INITIALIZER,
1007
- useFactory: (permissionValidator: PermissionValidatorService) => {
1008
- return () => {
1009
- // Set wildcard permissions when IAM is disabled
1010
- permissionValidator.setPermissions(['*']);
1011
- };
1012
- },
1013
- deps: [PermissionValidatorService],
1014
- multi: true,
1015
- },
1016
- // ... other providers
1017
- ],
1018
- };
1348
+ { provide: AUTH_USER_LIST_EXTENSION, useFactory: (router, msg) => new MyExtension(router, msg), deps: [Router, MessageService] }
1019
1349
  ```
1020
1350
 
1021
- #### Option 3: In AuthInitService (After Session Restore)
1351
+ ### AUTH_USER_DETAIL_CONFIG
1022
1352
 
1023
- If you restore sessions on page refresh, set permissions there:
1353
+ Configure user detail view mode.
1354
+
1355
+ **IUserDetailViewConfig:** `viewMode` (dialog|sidebar|page), `allowEdit?`, `width?`, `component?`, `tabs?` (array: key, label, icon?, component?, template?)
1024
1356
 
1025
1357
  ```typescript
1026
- // After successful /auth/me response
1027
- authInit.initialize().subscribe({
1028
- next: (user) => {
1029
- // Session restored, set wildcard permissions
1030
- permissionValidator.setPermissions(['*']);
1031
- },
1032
- });
1358
+ { provide: AUTH_USER_DETAIL_CONFIG, useValue: { viewMode: 'dialog', allowEdit: true, width: '800px' } }
1033
1359
  ```
1034
1360
 
1035
- ### Wildcard Permission Syntax
1361
+ ### AUTH_PROFILE_EXTENSION
1036
1362
 
1037
- The `permissionGuard` utilities support these patterns:
1363
+ Add extra fields and sections to profile page.
1038
1364
 
1039
- | Pattern | Meaning |
1040
- |---------|---------|
1041
- | `*` | Full access to everything |
1042
- | `user.*` | All user permissions |
1043
- | `branch.*` | All branch permissions |
1365
+ **IProfileExtraField:** `key`, `label`, `type` (text|number|select|checkbox|textarea|date|template), `placeholder?`, `editable?` (false=view-only), `required?`, `options?`, `pattern?`, `validationMessage?`, `order?`, `sectionKey?`, `template?`, `component?`, `injector?`
1366
+
1367
+ **IProfileExtraSection:** `key`, `title`, `icon?`, `order?`, `component?`, `template?`, `collapsible?`, `collapsed?`, `editPermission?`
1044
1368
 
1045
- ### For Jest Tests
1369
+ **IProfileExtensionProvider Methods:**
1046
1370
 
1047
- Mock `PermissionValidatorService` in your test setup:
1371
+ | Method | Purpose |
1372
+ |--------|---------|
1373
+ | `getExtraFields?()` | Return field definitions |
1374
+ | `getExtraSections?()` | Return section definitions |
1375
+ | `getExtraFieldValues?(userId)` | Fetch values from custom API |
1376
+ | `saveExtraFieldValues?(userId, data)` | Save values to custom API |
1377
+ | `validateExtraFields?(data)` | Custom validation, return error message or null |
1048
1378
 
1049
1379
  ```typescript
1050
- import { PermissionValidatorService } from '@flusys/ng-shared';
1051
-
1052
- describe('MyComponent', () => {
1053
- beforeEach(async () => {
1054
- await TestBed.configureTestingModule({
1055
- imports: [MyComponent],
1056
- providers: [
1057
- {
1058
- provide: PermissionValidatorService,
1059
- useValue: {
1060
- permissions: () => ['*'],
1061
- isPermissionsLoaded: () => true,
1062
- hasPermission: () => true,
1063
- setPermissions: jest.fn(),
1064
- clearPermissions: jest.fn(),
1065
- },
1066
- },
1067
- ],
1068
- }).compileComponents();
1069
- });
1070
- });
1380
+ { provide: AUTH_PROFILE_EXTENSION, useClass: MyProfileExtension }
1381
+ // Custom component: inject(PROFILE_FIELD_CONTEXT) for field, value, updateValue()
1071
1382
  ```
1072
1383
 
1073
- ### For Template Permission Checks
1384
+ ### Provider Factory Example
1074
1385
 
1075
- If using `*hasPermission` directive, it will also respect wildcard:
1386
+ Combine all extensions in a single provider function for app.config.ts:
1076
1387
 
1077
- ```html
1078
- <!-- This will show when permissions include '*' -->
1079
- <button *hasPermission="'user.delete'">Delete User</button>
1388
+ ```typescript
1389
+ export function provideAuthExtensions(): Provider[] {
1390
+ return [
1391
+ { provide: AUTH_VALIDATION_CONFIG, useValue: myValidationConfig },
1392
+ { provide: AUTH_REGISTRATION_EXTENSION, useClass: MyRegistrationExtension },
1393
+ { provide: AUTH_USER_LIST_EXTENSION, useFactory: (r, m) => new MyUserListExtension(r, m), deps: [Router, MessageService] },
1394
+ { provide: AUTH_USER_DETAIL_CONFIG, useValue: { viewMode: 'dialog', allowEdit: true } },
1395
+ { provide: AUTH_PROFILE_EXTENSION, useClass: MyProfileExtension },
1396
+ ];
1397
+ }
1398
+ // Usage: providers: [...provideAuthExtensions()]
1080
1399
  ```
1081
1400
 
1401
+ ### additionalFields Flow
1402
+
1403
+ When using extensions, registration data flows as:
1404
+
1405
+ 1. **Frontend:** `transformRegistrationData()` converts extra fields to `additionalFields`
1406
+ 2. **Backend:** Registration stores `additionalFields` in PendingRegistration (email flow) or passes to `USER_ENRICHER.onUserCreated`
1407
+ 3. **Backend:** `USER_ENRICHER.onUserCreated(userId, additionalFields, queryRunner)` creates related entities (e.g., UserProfileInfo) within the same transaction
1408
+ 4. **Error handling:** If `onUserCreated` throws, the entire registration rolls back
1409
+
1082
1410
  ---
1083
1411
 
1084
1412
  ## See Also
@@ -1091,25 +1419,8 @@ If using `*hasPermission` directive, it will also respect wildcard:
1091
1419
 
1092
1420
  ---
1093
1421
 
1094
- **Last Updated:** 2026-02-21
1422
+ **Last Updated:** 2026-02-25
1423
+ **Version:** 3.0.1
1095
1424
  **Angular Version:** 21
1096
1425
 
1097
- ## Component Checklist
1098
-
1099
- | Component | OnPush | Signal Forms | Responsive | Dark Mode |
1100
- |-----------|--------|--------------|------------|-----------|
1101
- | LoginPageComponent | ✅ | ✅ | ✅ | ✅ |
1102
- | RegisterPageComponent | ✅ | ✅ | ✅ | ✅ |
1103
- | ForgotPasswordPageComponent | ✅ | ✅ | ✅ | ✅ |
1104
- | ResetPasswordPageComponent | ✅ | ✅ | ✅ | ✅ |
1105
- | VerifyEmailPageComponent | ✅ | ✅ | ✅ | ✅ |
1106
- | ProfilePageComponent | ✅ | ✅ | ✅ | ✅ |
1107
- | AdministrationPageComponent | ✅ | N/A | ✅ | ✅ |
1108
- | UserListComponent | ✅ | N/A | ✅ | ✅ |
1109
- | UserFormComponent | ✅ | ✅ | ✅ | ✅ |
1110
- | CompanyListComponent | ✅ | N/A | ✅ | ✅ |
1111
- | CompanyFormComponent | ✅ | ✅ | ✅ | ✅ |
1112
- | BranchListComponent | ✅ | N/A | ✅ | ✅ |
1113
- | BranchFormComponent | ✅ | ✅ | ✅ | ✅ |
1114
- | UserCompanyPermissionDialogComponent | ✅ | N/A | ✅ | ✅ |
1115
- | UserBranchPermissionDialogComponent | ✅ | N/A | ✅ | ✅ |
1426
+ All 15 components use `ChangeDetectionStrategy.OnPush`, responsive design, and dark mode support. Form components use Signal Forms.