@flusys/ng-auth 4.0.1 → 4.1.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.
Files changed (2) hide show
  1. package/README.md +305 -1451
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,1622 +1,476 @@
1
- # @flusys/ng-auth Package Guide
1
+ # @flusys/ng-auth
2
2
 
3
- ## Overview
4
-
5
- `@flusys/ng-auth` provides complete authentication functionality including login, registration, token management, company/branch selection, user administration, and permission handling.
3
+ > Complete authentication library for the FLUSYS Angular platform — JWT session management, HTTP-only cookie tokens, multi-company selection, and provider adapter registration.
6
4
 
7
- **Package:** `@flusys/ng-auth`
8
- **Version:** 4.0.1
9
- **Dependencies:** ng-core, ng-shared, ng-layout
10
- **Build:** `npm run build:ng-auth`
11
-
12
- ## Architecture
13
-
14
- ```
15
- ┌─────────────────────────────────────────────────────────────┐
16
- │ ng-auth │
17
- ├─────────────────────────────────────────────────────────────┤
18
- │ Services │
19
- │ ├── AuthApiService (login, register, refresh, logout) │
20
- │ ├── AuthStateService (signal-based state management) │
21
- │ ├── AuthInitService (session restoration on refresh) │
22
- │ ├── TokenRefreshStateService (concurrent refresh handling) │
23
- │ ├── UserApiService (user CRUD) │
24
- │ ├── CompanyApiService (company CRUD) │
25
- │ ├── BranchApiService (branch CRUD) │
26
- │ └── UserPermissionApiService (permission assignments) │
27
- ├─────────────────────────────────────────────────────────────┤
28
- │ Guards │
29
- │ ├── authGuard (requires authentication) │
30
- │ ├── guestGuard (allows only unauthenticated) │
31
- │ ├── companyFeatureGuard (requires company feature enabled) │
32
- │ ├── emailFeatureGuard (requires email service enabled) │
33
- │ └── signUpFeatureGuard (requires sign-up enabled) │
34
- ├─────────────────────────────────────────────────────────────┤
35
- │ Interceptors │
36
- │ ├── authInterceptor (adds JWT + tenant headers) │
37
- │ └── tokenRefreshInterceptor (proactive + reactive refresh) │
38
- ├─────────────────────────────────────────────────────────────┤
39
- │ Adapters (Provider Interface Pattern) │
40
- │ ├── AuthLayoutStateAdapter → LAYOUT_AUTH_STATE │
41
- │ ├── AuthLayoutApiAdapter → LAYOUT_AUTH_API │
42
- │ ├── AuthUserProviderAdapter → USER_PROVIDER │
43
- │ ├── AuthCompanyApiProviderAdapter → COMPANY_API_PROVIDER │
44
- │ ├── AuthUserPermissionProviderAdapter → USER_PERMISSION_PROVIDER │
45
- │ └── AuthStateProviderAdapter → AUTH_STATE_PROVIDER │
46
- ├─────────────────────────────────────────────────────────────┤
47
- │ Constants │
48
- │ └── AUTH_ENDPOINTS (endpoint path definitions) │
49
- └─────────────────────────────────────────────────────────────┘
50
- ```
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/ng-auth.svg)](https://www.npmjs.com/package/@flusys/ng-auth)
6
+ [![Angular](https://img.shields.io/badge/Angular-21-red.svg)](https://angular.io)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
51
9
 
52
10
  ---
53
11
 
54
- ## Services
55
-
56
- ### AuthApiService
57
-
58
- HTTP client for authentication endpoints. Uses `getServiceUrl(appConfig, 'auth')` for base URL.
59
-
60
- ```typescript
61
- import { AuthApiService } from '@flusys/ng-auth';
62
-
63
- const authApi = inject(AuthApiService);
64
-
65
- // Authentication
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
71
-
72
- // Company/Branch selection
73
- authApi.select({ companyId, branchId, sessionId }); // POST /auth/select → ILoginResponse
74
- authApi.switchCompany({ companyId, branchId }); // POST /auth/switch-company → ILoginResponse
75
-
76
- // Password management
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
- }
107
- ```
108
-
109
- ### AuthStateService
110
-
111
- Signal-based centralized auth state management with auto-persistence.
112
-
113
- **Security Note:** Access tokens are stored in memory only (not localStorage) to prevent XSS attacks. Only token expiry time is persisted.
114
-
115
- ```typescript
116
- import { AuthStateService } from '@flusys/ng-auth';
117
-
118
- const authState = inject(AuthStateService);
119
-
120
- // Read-only signals
121
- authState.user(); // Signal<IUserInfo | null>
122
- authState.accessToken(); // Signal<string> (memory-only)
123
- authState.tokenExpiresAt(); // Signal<number | null> (milliseconds timestamp)
124
- authState.company(); // Signal<ICompanyInfo | null>
125
- authState.branch(); // Signal<IBranchInfo | null>
126
- authState.selectionSessionId(); // Signal<string | null>
127
- authState.availableCompanies(); // Signal<ICompanyWithBranches[]>
128
-
129
- // Computed signals
130
- authState.isAuthenticated(); // Signal<boolean> - has token AND user
131
- authState.requiresCompanySelection(); // Signal<boolean>
132
- authState.userName(); // Signal<string>
133
- authState.companyName(); // Signal<string>
134
- authState.branchName(); // Signal<string>
135
- authState.tenantId(); // Signal<string | null>
136
- authState.isTokenExpired(); // Signal<boolean> - true if expired or expiring within 30s
137
-
138
- // State mutations
139
- authState.setLoginState(response); // After successful login
140
- authState.setSelectionState(response); // After login requires selection
141
- authState.clearSelectionState(); // Clear pending selection
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
151
- authState.navigateToLogin(returnUrl?);
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
157
- ```
158
-
159
- **Auto-persistence via effects:**
160
- - `tokenExpiresAt` → localStorage `tokenExpiresAt`
161
- - `company.id` → localStorage `selectedCompanyId`
162
- - `branch.id` → localStorage `selectedBranchId`
163
-
164
- ### AuthInitService
165
-
166
- Handles session restoration on page refresh.
167
-
168
- ```typescript
169
- import { AuthInitService } from '@flusys/ng-auth';
170
-
171
- const authInit = inject(AuthInitService);
172
-
173
- // Signals
174
- authInit.initialized(); // Signal<boolean> - true after init attempt
175
- authInit.loading(); // Signal<boolean>
176
- authInit.error(); // Signal<string | null>
177
-
178
- // Restore session (called by guards)
179
- authInit.initialize().subscribe({
180
- next: (success) => console.log('Session restored:', success),
181
- error: (err) => console.log('No session or expired')
182
- });
183
- ```
184
-
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
- ```
213
-
214
- ### Resource API Services
215
-
216
- All extend `ApiResourceService` from ng-shared with signal-based state.
217
-
218
- ```typescript
219
- import { UserApiService, CompanyApiService, BranchApiService } from '@flusys/ng-auth';
220
-
221
- // Common signals (from ApiResourceService)
222
- service.data(); // Signal<T[]>
223
- service.isLoading(); // Signal<boolean>
224
- service.error(); // Signal<string | null>
225
- service.total(); // Signal<number>
226
-
227
- // Common methods
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();
234
- ```
235
-
236
- **UserApiService** - Endpoint: `administration/users`
237
- ```typescript
238
- const userApi = inject(UserApiService);
239
- userApi.updateProfile(data); // POST /administration/users/profile
240
- ```
241
-
242
- **CompanyApiService** - Endpoint: `administration/company`
243
- ```typescript
244
- const companyApi = inject(CompanyApiService);
245
- // Standard CRUD via ApiResourceService
246
- ```
247
-
248
- **BranchApiService** - Endpoint: `administration/branch`
249
- ```typescript
250
- const branchApi = inject(BranchApiService);
251
- branchApi.setCurrentCompanyId(companyId); // Set company context
252
- branchApi.fetchByCompany(companyId, search); // Fetch with company filter
253
- ```
254
-
255
- ### UserPermissionApiService
256
-
257
- Manages user-to-company and user-to-branch permission assignments.
258
-
259
- ```typescript
260
- import { UserPermissionApiService } from '@flusys/ng-auth';
261
-
262
- const permApi = inject(UserPermissionApiService);
263
-
264
- // Company Permissions
265
- permApi.assignUserCompanies({
266
- userId: 'user-1',
267
- items: [
268
- { targetId: 'company-1', isAdd: true }, // Assign
269
- { targetId: 'company-2', isAdd: false } // Revoke
270
- ]
271
- });
272
- permApi.getUserCompanies(userId); // POST /administration/permissions/get-user-company
273
- permApi.assignUserToCompany(userId, companyId); // Single assign
274
- permApi.revokeUserFromCompany(userId, companyId); // Single revoke
275
-
276
- // Branch Permissions
277
- permApi.assignUserBranches({
278
- userId: 'user-1',
279
- items: [{ targetId: 'branch-1', isAdd: true }]
280
- });
281
- permApi.getUserBranches(userId, companyId?); // POST /administration/permissions/get-user-branch
282
- permApi.assignUserToBranch(userId, branchId);
283
- permApi.revokeUserFromBranch(userId, branchId);
284
- ```
285
-
286
- **Backend Endpoints:**
287
- - `POST /administration/permissions/user-company/assign`
288
- - `POST /administration/permissions/get-user-company`
289
- - `POST /administration/permissions/user-branch/assign`
290
- - `POST /administration/permissions/get-user-branch`
12
+ ## Table of Contents
13
+
14
+ - [Overview](#overview)
15
+ - [Features](#features)
16
+ - [Compatibility](#compatibility)
17
+ - [Installation](#installation)
18
+ - [Quick Start](#quick-start)
19
+ - [Module Registration](#module-registration)
20
+ - [Session Restoration Flow](#session-restoration-flow)
21
+ - [Auth State Service](#auth-state-service)
22
+ - [Auth API Service](#auth-api-service)
23
+ - [HTTP Interceptors](#http-interceptors)
24
+ - [Guards](#guards)
25
+ - [Pages](#pages)
26
+ - [Provider Adapters](#provider-adapters)
27
+ - [API Endpoints](#api-endpoints)
28
+ - [Configuration Reference](#configuration-reference)
29
+ - [Troubleshooting](#troubleshooting)
30
+ - [License](#license)
291
31
 
292
32
  ---
293
33
 
294
- ## Guards
295
-
296
- ### authGuard
297
-
298
- Protects routes requiring authentication.
299
-
300
- ```typescript
301
- import { authGuard } from '@flusys/ng-auth';
302
-
303
- {
304
- path: 'dashboard',
305
- canActivate: [authGuard],
306
- loadComponent: () => import('./dashboard.component')
307
- }
308
- ```
309
-
310
- **Checks:**
311
- 1. User is authenticated (`isAuthenticated()`)
312
- 2. If company feature enabled: company AND branch are selected
313
- 3. Redirects to `/auth/login` with `returnUrl` query param if checks fail
314
-
315
- ### guestGuard
316
-
317
- Protects auth routes (login, register) from authenticated users. Restores session first.
318
-
319
- ```typescript
320
- import { guestGuard } from '@flusys/ng-auth';
321
-
322
- {
323
- path: 'auth/login',
324
- canActivate: [guestGuard],
325
- loadComponent: () => import('./login.component')
326
- }
327
- ```
328
-
329
- **Behavior:**
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
335
-
336
- ### companyFeatureGuard
337
-
338
- Enables/disables company management routes based on config.
339
-
340
- ```typescript
341
- import { companyFeatureGuard } from '@flusys/ng-auth';
342
-
343
- {
344
- path: 'administration/company',
345
- canActivate: [authGuard, companyFeatureGuard],
346
- loadComponent: () => import('./company-list.component')
347
- }
348
- ```
349
-
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`.
383
-
384
- ---
385
-
386
- ## Interceptors
387
-
388
- ### authInterceptor
389
-
390
- Adds authentication headers to HTTP requests.
391
-
392
- ```typescript
393
- import { authInterceptor } from '@flusys/ng-auth';
394
-
395
- provideHttpClient(
396
- withInterceptors([authInterceptor, ...])
397
- )
398
- ```
399
-
400
- **Adds:**
401
- - `Authorization: Bearer {token}` if token exists
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`
408
-
409
- ### tokenRefreshInterceptor
410
-
411
- Handles 401 responses and proactive token refresh.
412
-
413
- ```typescript
414
- import { tokenRefreshInterceptor } from '@flusys/ng-auth';
415
-
416
- provideHttpClient(
417
- withInterceptors([authInterceptor, tokenRefreshInterceptor, ...])
418
- )
419
- ```
420
-
421
- **Behavior:**
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
436
-
437
- ---
438
-
439
- ## Constants
440
-
441
- ### AUTH_ENDPOINTS
442
-
443
- Centralized endpoint definitions used by interceptors.
444
-
445
- ```typescript
446
- import { AUTH_ENDPOINTS, isEndpoint } from '@flusys/ng-auth';
447
-
448
- // Endpoint constants
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'
461
-
462
- // Helper function
463
- isEndpoint(url, endpoint); // Check if URL contains endpoint
464
- ```
465
-
466
- ---
467
-
468
- ## Provider Adapters
469
-
470
- ng-auth implements provider interfaces from ng-shared and ng-layout, enabling feature packages to access auth data without direct dependencies.
471
-
472
- ### provideAuthProviders()
473
-
474
- Registers provider adapters for ng-shared interfaces (used by ng-iam, ng-storage, ng-form-builder).
475
-
476
- ```typescript
477
- import { provideAuthProviders } from '@flusys/ng-auth';
478
-
479
- // app.config.ts
480
- providers: [
481
- ...provideAuthProviders()
482
- ]
483
- ```
484
-
485
- **Registers:**
486
- - `USER_PROVIDER` → `AuthUserProviderAdapter` (user list for ng-iam)
487
- - `COMPANY_API_PROVIDER` → `AuthCompanyApiProviderAdapter` (company list for ng-iam)
488
- - `USER_PERMISSION_PROVIDER` → `AuthUserPermissionProviderAdapter` (permission queries)
489
- - `AUTH_STATE_PROVIDER` → `AuthStateProviderAdapter` (isAuthenticated, initialize)
490
-
491
- ### provideAuthLayoutIntegration()
492
-
493
- Bridges ng-auth to ng-layout components.
494
-
495
- ```typescript
496
- import { provideAuthLayoutIntegration } from '@flusys/ng-auth';
497
-
498
- // app.config.ts
499
- providers: [
500
- ...provideAuthLayoutIntegration()
501
- ]
502
- ```
34
+ ## Overview
503
35
 
504
- **Registers:**
505
- - `LAYOUT_AUTH_STATE` → `AuthLayoutStateAdapter` (user/company/branch signals)
506
- - `LAYOUT_AUTH_API` → `AuthLayoutApiAdapter` (logout, switch company, etc.)
36
+ `@flusys/ng-auth` provides end-to-end authentication for FLUSYS Angular applications. It handles the complete lifecycle: login, registration, token refresh, session restoration on page reload, email verification, profile management, password changes, and multi-company selection.
507
37
 
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
-
515
- **AuthLayoutApiAdapter Methods:**
516
- ```typescript
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
522
- ```
38
+ Tokens are stored in **HTTP-only cookies** (managed by the backend) — never in localStorage. The access token is held in memory only and automatically refreshed via a silent refresh interceptor.
523
39
 
524
40
  ---
525
41
 
526
- ## Pages
42
+ ## Features
527
43
 
528
- All page components follow Angular 21 best practices:
529
- - Signal Forms (`@angular/forms/signals`) with validation
530
- - 100% responsive design (mobile-first with Tailwind)
531
- - Dark mode compatible (PrimeNG + Tailwind CSS variables)
532
- - `inject()` for dependency injection
533
- - `effect()` for reactive data loading
534
-
535
- ### Authentication Pages
536
-
537
- **LoginPageComponent** (`/auth/login`)
538
- - Multi-step login flow with visual step indicator (when company feature enabled)
539
- - Step 1: Credentials (email/password)
540
- - Step 2: Company/Branch selection (if `requiresSelection: true`)
541
- - Email verification step (if `requiresEmailVerification: true`)
542
- - Auto-selects if only one company/branch available
543
- - Restores stored company/branch from localStorage
544
- - Stores returnUrl for post-login redirect
545
- - Support for AUTH_VALIDATION_CONFIG (custom email/password validation)
546
- - **Conditional UI:**
547
- - "Forgot password?" link hidden when `services.email.enabled: false`
548
- - "Sign Up" link hidden when `enableSignUp: false`
549
-
550
- **RegisterPageComponent** (`/auth/register`)
551
- - User registration with optional company creation
552
- - Two-column responsive grid layout
553
- - Email/password validation with AUTH_VALIDATION_CONFIG support
554
- - Company details section (when company feature enabled)
555
- - Support for AUTH_REGISTRATION_EXTENSION (extra fields)
556
- - **Default company/branch support:**
557
- - When `defaultCompanySlug` is set: pre-fills company code field (readonly)
558
- - When `defaultBranchSlug` is set: pre-fills branch code field (readonly)
559
- - "Create new company" option hidden when defaults are configured
560
- - Query param support: `?companySlug=xxx`
561
-
562
- **ForgotPasswordPageComponent** (`/auth/forgot-password`)
563
- - Email input with validation
564
- - Success state showing email sent confirmation
565
- - Always shows success (security: don't reveal if email exists)
566
-
567
- **ResetPasswordPageComponent** (`/auth/reset-password`)
568
- - Password reset with token from query param `?token=xxx`
569
- - Password confirmation matching
570
- - Three states: `form`, `success`, `invalid-token`
571
- - Auto-detects expired/invalid token from error message
572
-
573
- **VerifyEmailPageComponent** (`/auth/verify-email`)
574
- - Token verification from query param `?token=xxx`
575
- - SSR support via TransferState (prevents duplicate API calls)
576
- - Four states: `verifying`, `success`, `invalid-token`, `resend`
577
- - Resend verification form when token invalid/expired
578
-
579
- ### Profile Page
580
-
581
- **ProfilePageComponent** (`/profile`)
582
- - Page title with description
583
- - Avatar section with initials fallback
584
- - Profile picture upload (requires `FILE_PROVIDER` from ng-storage)
585
- - Update name and phone with Signal Forms
586
- - Extra fields support via two providers:
587
- - `AUTH_PROFILE_EXTENSION` - Fields user can edit
588
- - `AUTH_USER_FORM_EXTENSION` - Fields displayed as view-only ("Managed by Admin")
589
- - Change password section with PrimeNG password strength indicator
590
- - Company/branch info (if company feature enabled)
591
- - Roles/actions display (requires `PROFILE_PERMISSION_PROVIDER` from ng-iam)
592
- - Permission mode support: `rbac`, `direct`, `full`
593
- - Responsive two-column layout on desktop
594
-
595
- **Optional Provider Dependencies:**
596
-
597
- | Provider | Source | Feature Enabled |
598
- |----------|--------|-----------------|
599
- | `FILE_PROVIDER` | ng-storage via `provideStorageProviders()` | Profile picture upload |
600
- | `PROFILE_PERMISSION_PROVIDER` | ng-iam via `provideIamProviders()` | Roles/actions display |
601
-
602
- ### Administration Pages
603
-
604
- **AdministrationPageComponent** (`/administration`)
605
- - Page title "Administration" with dynamic subtitle
606
- - Horizontal scrolling tabs with hidden scrollbar
607
- - Tabs: Users, Companies, Branches
608
- - Company/Branch tabs only visible when company feature enabled
609
- - Permission-based tab visibility (USER_PERMISSIONS.READ, etc.)
610
-
611
- **User Management:**
612
- - `UserListComponent` - LazyLoad datatable with Edit, Delete, View Details, Permission actions
613
- - Extra columns/actions support via AUTH_USER_LIST_EXTENSION
614
- - User detail view via AUTH_USER_DETAIL_CONFIG (dialog or page mode)
615
- - `UserFormComponent` - Create/edit user
616
- - Auto-assigns new users to current company/branch when company feature enabled
617
-
618
- **Company Management:**
619
- - `CompanyListComponent` - Company CRUD with View Branches action
620
- - readOnly companies cannot be deleted
621
- - `CompanyFormComponent` - Create/edit company with auto slug generation
622
-
623
- **Branch Management:**
624
- - `BranchListComponent` - Branch CRUD filtered by company dropdown
625
- - Defaults to current company context
626
- - Supports `?companyId=xxx` query param
627
- - `BranchFormComponent` - Create/edit branch with company selection
628
-
629
- **Table Features (all list components):**
630
- - Responsive header (stacked on mobile)
631
- - Horizontal scroll wrapper (`overflow-x-auto`)
632
- - Conditional pagination (`[paginator]="total() > 0"`)
633
- - Striped rows (`p-datatable-striped`)
634
- - Lazy loading for performance
44
+ - ✅ JWT authentication with HTTP-only refresh token cookies
45
+ - ✅ Silent token refresh on 401 responses
46
+ - ✅ Session restoration on page reload (`AuthInitService`)
47
+ - ✅ Multi-company selection flow
48
+ - ✅ Email verification with OTP
49
+ - ✅ Profile management (name, password, avatar)
50
+ - ✅ `AuthStateService` — signal-based reactive auth state
51
+ - ✅ `provideAuthProviders()` — registers `USER_PROVIDER`, `COMPANY_PROVIDER`
52
+ - ✅ `provideAuthLayoutIntegration()` — bridges auth to layout
53
+ - ✅ Guard: `appInitGuard` for session restoration before navigation
54
+ - ✅ Guard: `AuthenticatedGuard`, `GuestGuard`, `CompanySelectedGuard`
635
55
 
636
56
  ---
637
57
 
638
- ## Dialog Components
639
-
640
- Both dialog components use Angular 21 patterns:
641
- - `input.required<T>()` for required inputs
642
- - `output<void>()` for events
643
- - `effect()` for reactive data loading
644
- - Responsive breakpoints (`{ '960px': '90vw', '640px': '95vw' }`)
645
-
646
- ### UserCompanyPermissionDialogComponent
647
-
648
- Manages user-to-company assignments with checkboxes.
649
-
650
- ```typescript
651
- import { UserCompanyPermissionDialogComponent } from '@flusys/ng-auth';
652
-
653
- @Component({
654
- imports: [UserCompanyPermissionDialogComponent],
655
- template: `
656
- <lib-user-company-permission-dialog
657
- [visible]="showDialog()"
658
- [user]="selectedUser()"
659
- (closed)="showDialog.set(false)"
660
- (permissionsChanged)="reloadData()" />
661
- `
662
- })
663
- ```
664
-
665
- **Inputs/Outputs:**
666
- ```typescript
667
- readonly visible = input.required<boolean>();
668
- readonly user = input.required<IUser | null>();
669
- readonly closed = output<void>();
670
- readonly permissionsChanged = output<void>();
671
- ```
58
+ ## Compatibility
672
59
 
673
- **Features:**
674
- - Shows all companies with checkboxes
675
- - Pre-checks assigned companies
676
- - Immediate save on toggle (no Save button)
677
- - Toast notifications for success/error
678
- - Loading spinner during data fetch
679
- - Disabled checkboxes while saving
680
-
681
- ### UserBranchPermissionDialogComponent
682
-
683
- Manages user-to-branch assignments with company filtering.
684
-
685
- ```typescript
686
- import { UserBranchPermissionDialogComponent } from '@flusys/ng-auth';
687
-
688
- @Component({
689
- imports: [UserBranchPermissionDialogComponent],
690
- template: `
691
- <lib-user-branch-permission-dialog
692
- [visible]="showDialog()"
693
- [user]="selectedUser()"
694
- (closed)="showDialog.set(false)"
695
- (permissionsChanged)="reloadData()" />
696
- `
697
- })
698
- ```
699
-
700
- **Features:**
701
- - Company dropdown shows only user's permitted companies
702
- - Branch list loads on company selection
703
- - Immediate save on toggle
704
- - Contextual empty messages:
705
- - "This user has no company permissions assigned" (no companies)
706
- - "Please select a company to view branches" (no selection)
707
- - "No branches available" (empty)
60
+ | Package | Version |
61
+ |---------|---------|
62
+ | Angular | 21+ |
63
+ | @flusys/ng-core | 4.x |
64
+ | @flusys/ng-shared | 4.x |
708
65
 
709
66
  ---
710
67
 
711
- ## Routes
712
-
713
- ### AUTH_ROUTES
714
-
715
- Public authentication routes.
716
-
717
- ```typescript
718
- import { AUTH_ROUTES } from '@flusys/ng-auth';
719
-
720
- // Routes: /auth/login, /auth/register, /auth/forgot-password, /auth/reset-password, /auth/verify-email
721
- { path: 'auth', children: AUTH_ROUTES }
722
- ```
723
-
724
- **Route Configuration:**
725
- | Path | Component | Guards |
726
- |------|-----------|--------|
727
- | `/auth/login` | LoginPageComponent | guestGuard |
728
- | `/auth/register` | RegisterPageComponent | signUpFeatureGuard, guestGuard |
729
- | `/auth/forgot-password` | ForgotPasswordPageComponent | emailFeatureGuard, guestGuard |
730
- | `/auth/reset-password` | ResetPasswordPageComponent | emailFeatureGuard, guestGuard |
731
- | `/auth/verify-email` | VerifyEmailPageComponent | emailFeatureGuard |
732
-
733
- ### ADMINISTRATION_ROUTES
734
-
735
- Protected administration routes with tabbed interface.
736
-
737
- ```typescript
738
- import { ADMINISTRATION_ROUTES } from '@flusys/ng-auth';
739
-
740
- {
741
- path: 'administration',
742
- children: ADMINISTRATION_ROUTES
743
- }
744
- ```
745
-
746
- **Route Configuration:**
747
- | Path | Component | Guards |
748
- |------|-----------|--------|
749
- | `/administration` | AdministrationPageComponent | authGuard |
750
- | `/administration/users` | UserListComponent | permissionGuard(USER_PERMISSIONS.READ) |
751
- | `/administration/users/new` | UserFormComponent | |
752
- | `/administration/users/:id` | UserFormComponent | |
753
- | `/administration/company` | CompanyListComponent | companyFeatureGuard, permissionGuard(COMPANY_PERMISSIONS.READ) |
754
- | `/administration/company/new` | CompanyFormComponent | |
755
- | `/administration/company/:id` | CompanyFormComponent | |
756
- | `/administration/branch` | BranchListComponent | companyFeatureGuard, permissionGuard(BRANCH_PERMISSIONS.READ) |
757
- | `/administration/branch/new` | BranchFormComponent | |
758
- | `/administration/branch/:id` | BranchFormComponent | |
759
-
760
- ### PROFILE_ROUTES
68
+ ## Installation
761
69
 
762
- User profile page routes.
763
-
764
- ```typescript
765
- import { PROFILE_ROUTES } from '@flusys/ng-auth';
766
-
767
- {
768
- path: 'profile',
769
- children: PROFILE_ROUTES
770
- }
70
+ ```bash
71
+ npm install @flusys/ng-auth @flusys/ng-core @flusys/ng-shared
771
72
  ```
772
73
 
773
74
  ---
774
75
 
775
- ## Setup
76
+ ## Quick Start
776
77
 
777
- ### app.config.ts
78
+ ### 1. Register Auth Providers
778
79
 
779
80
  ```typescript
81
+ // app.config.ts
780
82
  import { ApplicationConfig } from '@angular/core';
781
- import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
782
- import { APP_CONFIG, errorCatchingInterceptor } from '@flusys/ng-core';
83
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
84
+ import { APP_CONFIG } from '@flusys/ng-core';
783
85
  import {
86
+ provideAuthProviders,
87
+ provideAuthLayoutIntegration,
784
88
  authInterceptor,
785
89
  tokenRefreshInterceptor,
786
- provideAuthLayoutIntegration,
787
- provideAuthProviders
788
90
  } from '@flusys/ng-auth';
789
- import { provideIamProviders } from '@flusys/ng-iam';
790
- import { provideStorageProviders } from '@flusys/ng-storage';
91
+ import { environment } from './environments/environment';
791
92
 
792
93
  export const appConfig: ApplicationConfig = {
793
94
  providers: [
95
+ { provide: APP_CONFIG, useValue: environment },
794
96
  provideHttpClient(
795
- withFetch(),
796
97
  withInterceptors([
797
- authInterceptor, // First: adds token
798
- tokenRefreshInterceptor, // Second: handles 401 + proactive refresh
799
- errorCatchingInterceptor // Last: shows errors
98
+ authInterceptor, // Attaches access token to requests
99
+ tokenRefreshInterceptor, // Handles 401 → refresh → retry
800
100
  ])
801
101
  ),
802
- { provide: APP_CONFIG, useValue: DEFAULT_APP_CONFIG },
803
- ...provideAuthLayoutIntegration(),
804
- ...provideAuthProviders(),
805
- ...provideIamProviders(), // Enables permissions display on profile
806
- ...provideStorageProviders(), // Enables profile picture upload
807
- ]
102
+ ...provideAuthProviders(), // USER_PROVIDER, COMPANY_PROVIDER
103
+ ...provideAuthLayoutIntegration(), // LAYOUT_AUTH_STATE, LAYOUT_AUTH_API
104
+ ],
808
105
  };
809
106
  ```
810
107
 
811
- ### app.routes.ts
108
+ ### 2. Protect Routes with Guards
812
109
 
813
110
  ```typescript
111
+ // app.routes.ts
814
112
  import { Routes } from '@angular/router';
815
- import { authGuard, AUTH_ROUTES, ADMINISTRATION_ROUTES, PROFILE_ROUTES } from '@flusys/ng-auth';
113
+ import { appInitGuard } from '@flusys/ng-auth';
114
+ import { AppLayoutComponent } from '@flusys/ng-layout';
816
115
 
817
116
  export const routes: Routes = [
818
- { path: 'auth', children: AUTH_ROUTES },
819
117
  {
820
118
  path: '',
821
- canActivate: [authGuard],
119
+ component: AppLayoutComponent,
120
+ canActivate: [appInitGuard], // Restores session on page refresh
822
121
  children: [
823
- { path: 'dashboard', loadComponent: () => import('./dashboard.component') },
824
- { path: 'administration', children: ADMINISTRATION_ROUTES },
825
- { path: 'profile', children: PROFILE_ROUTES }
826
- ]
827
- }
122
+ { path: 'dashboard', loadComponent: () => import('./pages/dashboard.component') },
123
+ ],
124
+ },
125
+ {
126
+ path: 'auth',
127
+ loadChildren: () => import('@flusys/ng-auth').then(m => m.AUTH_ROUTES),
128
+ },
828
129
  ];
829
130
  ```
830
131
 
831
- ---
832
-
833
- ## Interfaces
834
-
835
- ### Request DTOs
132
+ ### 3. Enable Auth in App Config
836
133
 
837
134
  ```typescript
838
- interface ILoginRequest {
839
- email: string;
840
- password: string;
841
- }
842
-
843
- interface IRegistrationRequest {
844
- name: string;
845
- email: string;
846
- password: string;
847
- phone?: string;
848
- companySlug?: string;
849
- branchSlug?: string;
850
- newCompanyName?: string;
851
- newCompanyPhone?: string;
852
- newCompanyAddress?: string;
853
- additionalFields?: Record<string, unknown>;
854
- }
855
-
856
- interface ISelectRequest {
857
- sessionId: string;
858
- companyId: string;
859
- branchId: string;
860
- }
861
-
862
- interface ISwitchCompanyRequest {
863
- companyId: string;
864
- branchId: string;
865
- }
866
-
867
- interface IChangePasswordRequest {
868
- currentPassword: string;
869
- newPassword: string;
870
- }
871
-
872
- interface ICreateUserRequest {
873
- name: string;
874
- email: string;
875
- password: string;
876
- phone?: string;
877
- profilePictureId?: string;
878
- isActive?: boolean;
879
- emailVerified?: boolean;
880
- phoneVerified?: boolean;
881
- additionalFields?: Record<string, unknown>;
882
- }
883
-
884
- interface IUpdateUserRequest {
885
- id: string;
886
- name?: string;
887
- email?: string;
888
- password?: string;
889
- oldPassword?: string;
890
- phone?: string;
891
- profilePictureId?: string;
892
- isActive?: boolean;
893
- emailVerified?: boolean;
894
- phoneVerified?: boolean;
895
- additionalFields?: Record<string, unknown>;
896
- }
897
-
898
- interface ICreateCompanyRequest {
899
- name: string;
900
- slug: string;
901
- logoId?: string;
902
- address?: string;
903
- phone?: string;
904
- email?: string;
905
- website?: string;
906
- serial?: number;
907
- isActive?: boolean;
908
- settings?: Record<string, unknown>;
909
- additionalFields?: Record<string, unknown>;
910
- }
911
-
912
- interface ICreateBranchRequest {
913
- name: string;
914
- slug: string;
915
- companyId: string;
916
- parentId?: string;
917
- logoId?: string;
918
- address?: string;
919
- phone?: string;
920
- email?: string;
921
- serial?: number;
922
- isActive?: boolean;
923
- additionalFields?: Record<string, unknown>;
924
- }
135
+ // environments/environment.ts
136
+ export const environment = {
137
+ apiBaseUrl: 'http://localhost:2002',
138
+ services: {
139
+ auth: {
140
+ enabled: true,
141
+ features: {
142
+ signUp: true,
143
+ emailVerification: true,
144
+ companySelection: true,
145
+ },
146
+ },
147
+ },
148
+ };
925
149
  ```
926
150
 
927
- ### Response DTOs
928
-
929
- ```typescript
930
- interface IUserInfo {
931
- id: string;
932
- email: string;
933
- name?: string;
934
- phone?: string | null;
935
- profilePictureId?: string | null;
936
- companyId?: string;
937
- branchId?: string;
938
- companyLogoId?: string;
939
- branchLogoId?: string;
940
- }
941
-
942
- interface ICompanyInfo {
943
- id: string;
944
- name: string;
945
- slug?: string;
946
- logoId?: string | null;
947
- }
948
-
949
- interface IBranchInfo {
950
- id: string;
951
- name: string;
952
- slug?: string;
953
- logoId?: string | null;
954
- }
955
-
956
- interface ICompanyWithBranches {
957
- id: string;
958
- name: string;
959
- slug: string;
960
- logoId?: string | null;
961
- branches: IBranchSelectionInfo[];
962
- }
963
-
964
- interface ILoginResponse {
965
- accessToken: string;
966
- refreshToken: string;
967
- user: IUserInfo;
968
- company?: ICompanyInfo;
969
- branch?: IBranchInfo;
970
- }
971
-
972
- interface ICompanySelectionResponse {
973
- requiresSelection: true;
974
- sessionId: string;
975
- expiresAt: Date | string;
976
- user: IUserInfo;
977
- companies: ICompanyWithBranches[];
978
- }
979
-
980
- interface ILoginRequiresEmailVerification {
981
- requiresEmailVerification: true;
982
- email: string;
983
- message: string;
984
- }
985
-
986
- interface IMeResponse {
987
- user: IUserInfo;
988
- company?: ICompanyInfo;
989
- branch?: IBranchInfo;
990
- }
151
+ ---
991
152
 
992
- interface IRegistrationResponse {
993
- accessToken?: string;
994
- refreshToken?: string;
995
- user: IUserInfo;
996
- company?: ICompanyInfo;
997
- branch?: IBranchInfo;
998
- requiresEmailVerification?: boolean;
999
- }
153
+ ## Module Registration
1000
154
 
1001
- interface IRefreshTokenResponse {
1002
- accessToken: string;
1003
- refreshToken?: string;
1004
- }
155
+ `ng-auth` uses function-based providers (not Angular modules):
1005
156
 
1006
- type AuthResponse = ILoginResponse | ICompanySelectionResponse | ILoginRequiresEmailVerification;
1007
- ```
157
+ ### `provideAuthProviders()`
1008
158
 
1009
- ### Entity DTOs
159
+ Registers auth adapters against shared provider tokens:
1010
160
 
1011
161
  ```typescript
1012
- interface IUser {
1013
- id: string;
1014
- name: string;
1015
- email: string;
1016
- phone?: string | null;
1017
- profilePictureId?: string | null;
1018
- isActive: boolean;
1019
- emailVerified: boolean;
1020
- phoneVerified: boolean;
1021
- lastLoginAt?: Date | string | null;
1022
- additionalFields?: Record<string, unknown> | null;
1023
- createdAt: Date | string;
1024
- updatedAt: Date | string;
1025
- deletedAt?: Date | string | null;
1026
- }
162
+ ...provideAuthProviders()
1027
163
 
1028
- interface ICompany {
1029
- id: string;
1030
- name: string;
1031
- slug: string;
1032
- logoId?: string | null;
1033
- address?: string | null;
1034
- phone?: string | null;
1035
- email?: string | null;
1036
- website?: string | null;
1037
- serial?: number;
1038
- isActive: boolean;
1039
- readOnly: boolean;
1040
- settings?: Record<string, unknown> | null;
1041
- additionalFields?: Record<string, unknown> | null;
1042
- createdAt: Date | string;
1043
- updatedAt: Date | string;
1044
- }
1045
-
1046
- interface IBranch {
1047
- id: string;
1048
- name: string;
1049
- slug: string;
1050
- companyId: string;
1051
- parentId?: string | null;
1052
- logoId?: string | null;
1053
- address?: string | null;
1054
- phone?: string | null;
1055
- email?: string | null;
1056
- serial?: number;
1057
- isActive: boolean;
1058
- readOnly: boolean;
1059
- additionalFields?: Record<string, unknown> | null;
1060
- createdAt: Date | string;
1061
- updatedAt: Date | string;
1062
- }
164
+ // Provides:
165
+ // USER_PROVIDER → AuthStateService (IUserProvider)
166
+ // COMPANY_PROVIDER → AuthStateService (ICompanyProvider)
1063
167
  ```
1064
168
 
1065
- ### Permission DTOs
1066
-
1067
- ```typescript
1068
- interface IPermissionItem {
1069
- targetId: string;
1070
- isAdd: boolean;
1071
- }
1072
-
1073
- interface IAssignUserCompanyRequest {
1074
- userId: string;
1075
- items: IPermissionItem[];
1076
- }
1077
-
1078
- interface IAssignUserBranchRequest {
1079
- userId: string;
1080
- items: IPermissionItem[];
1081
- }
169
+ These tokens are consumed by `ng-iam`, `ng-storage`, `ng-notification` — no direct dependency on `ng-auth`.
1082
170
 
1083
- interface IBatchOperationResponse {
1084
- success: boolean;
1085
- message: string;
1086
- }
171
+ ### `provideAuthLayoutIntegration()`
1087
172
 
1088
- interface IUserCompanyPermission {
1089
- id: string;
1090
- userId: string;
1091
- companyId: string;
1092
- companyName: string;
1093
- companySlug: string;
1094
- }
173
+ Bridges auth state and API to the layout:
1095
174
 
1096
- interface IUserBranchPermission {
1097
- id: string;
1098
- userId: string;
1099
- branchId: string;
1100
- branchName: string;
1101
- branchSlug: string;
1102
- companyId: string;
1103
- }
1104
-
1105
- interface IUserCompaniesResponse {
1106
- success: boolean;
1107
- message: string;
1108
- data: IUserCompanyPermission[];
1109
- }
175
+ ```typescript
176
+ ...provideAuthLayoutIntegration()
1110
177
 
1111
- interface IUserBranchesResponse {
1112
- success: boolean;
1113
- message: string;
1114
- data: IUserBranchPermission[];
1115
- }
178
+ // Provides:
179
+ // LAYOUT_AUTH_STATE → AuthLayoutStateAdapter (profile picture, company logo)
180
+ // LAYOUT_AUTH_API → AuthLayoutApiAdapter (logout, select-company actions)
1116
181
  ```
1117
182
 
1118
183
  ---
1119
184
 
1120
- ## Best Practices
1121
-
1122
- ### Angular 21 Component Patterns
1123
-
1124
- All components follow these patterns:
1125
-
1126
- | Pattern | Implementation |
1127
- |---------|---------------|
1128
- | Dependency Injection | `inject()` function |
1129
- | State (private writable) | `private readonly _data = signal<T>(...)` |
1130
- | State (public readonly) | `readonly data = this._data.asReadonly()` |
1131
- | Computed | `readonly isValid = computed(() => ...)` |
1132
- | Signal Forms | `form(this.formModel, (f) => { required(f.field, {...}) })` |
1133
- | Reactive Init | `effect(() => { if (condition()) loadData(); })` |
1134
- | Async Methods | `try/finally` with `isLoading` signal, errors handled by global interceptor |
1135
-
1136
- ### Responsive Design Patterns
1137
-
1138
- ```html
1139
- <!-- Responsive container -->
1140
- <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 p-4">
185
+ ## Session Restoration Flow
1141
186
 
1142
- <!-- Two-column form grid -->
1143
- <form class="grid grid-cols-1 md:grid-cols-2 gap-4">
187
+ On every page refresh, auth state is cleared from memory. The `appInitGuard` restores the session transparently:
1144
188
 
1145
- <!-- Right-aligned buttons -->
1146
- <div class="flex justify-end gap-4 md:col-span-2 pt-4">
1147
-
1148
- <!-- Horizontally scrollable tabs -->
1149
- <div class="scrollbar-hide flex gap-1 overflow-x-auto flex-nowrap">
1150
-
1151
- <!-- Responsive table wrapper -->
1152
- <div class="overflow-x-auto">
1153
- <p-table [scrollable]="true" [paginator]="total() > 0">
1154
189
  ```
1155
-
1156
- ### Component Organization
1157
-
190
+ Page Refresh
191
+ │
192
+ ▼
193
+ appInitGuard runs
194
+ │
195
+ ├─ User in memory? ──YES──► Check company → Allow navigation
196
+ │
197
+ └─ NO ──► AuthInitService.initialize()
198
+ │
199
+ ├─ POST /auth/refresh (HTTP-only cookie) → access token
200
+ ├─ GET /auth/me → user data
201
+ └─ Session restored in memory
202
+ │
203
+ ├─ Auth enabled? → Check company selection
204
+ ├─ IAM enabled? → Load permissions
205
+ └─ Allow navigation
1158
206
  ```
1159
- projects/ng-auth/
1160
- ├── components/ ← Shared dialogs
1161
- │ ├── user-company-permission-dialog.component.ts
1162
- │ └── user-branch-permission-dialog.component.ts
1163
- ├── pages/ ← Route-specific pages
1164
- │ ├── login/
1165
- │ ├── register/
1166
- │ ├── forgot-password/
1167
- │ ├── reset-password/
1168
- │ ├── verify-email/
1169
- │ ├── profile/
1170
- │ ├── administration/
1171
- │ ├── users/
1172
- │ ├── company/
1173
- │ └── branch/
1174
- ├── services/
1175
- ├── guards/
1176
- ├── interceptors/
1177
- ├── adapters/
1178
- ├── providers/
1179
- ├── interfaces/
1180
- ├── constants/
1181
- └── routes/
1182
- ```
1183
-
1184
- **Rule:** Dialog components go in `components/`, page components go in `pages/`.
1185
-
1186
- ### Permission Filtering Pattern
1187
207
 
1188
- When building permission UI, filter by user's permitted scope:
208
+ ### Company Selection Flow
1189
209
 
1190
- ```typescript
1191
- // ✅ CORRECT - Filter by user's permitted companies
1192
- const response = await firstValueFrom(permApi.getUserCompanies(userId));
1193
- const userCompanies = response.data;
1194
-
1195
- // ❌ WRONG - Don't show all companies
1196
- const allCompanies = await companyApi.fetchList(); // Security risk
1197
210
  ```
1198
-
1199
- ### Token Management
1200
-
1201
- - Access tokens stored in memory only (XSS protection)
1202
- - Only token expiry is persisted to localStorage
1203
- - Refresh tokens sent via httpOnly cookies
1204
- - TokenRefreshInterceptor handles:
1205
- - Proactive refresh (30s before expiry)
1206
- - Reactive refresh (on 401)
1207
- - Request queuing during refresh
1208
- - Auto-persistence for company/branch selection
1209
-
1210
- ### Type Safety
1211
-
1212
- Avoid `any` types - use explicit interfaces:
1213
-
1214
- ```typescript
1215
- // ✅ CORRECT - Explicit response type
1216
- .subscribe({
1217
- next: (response: { success: boolean; data: IUser }) => {
1218
- if (response.success) { ... }
1219
- },
1220
- });
1221
-
1222
- // ❌ WRONG - Using any
1223
- .subscribe({
1224
- next: (response: any) => { ... },
1225
- });
1226
- ```
1227
-
1228
- ### Multi-Company Flow
1229
-
1230
- 1. Login → Check response type with type guards
1231
- 2. If `isCompanySelectionResponse` → Show Step 2 (company/branch selection)
1232
- 3. If `isEmailVerificationRequired` → Show email verification step
1233
- 4. If `isLoginResponse` → Direct login success
1234
- 5. Auto-select if only one company/branch
1235
- 6. Save selection → Navigate to dashboard
1236
-
1237
- ---
1238
-
1239
- ## Common Issues
1240
-
1241
- | Problem | Solution |
1242
- |---------|----------|
1243
- | Routes accessible without auth | Ensure `authGuard` is on parent route |
1244
- | 401 on all requests | Ensure interceptor order: `authInterceptor` → `tokenRefreshInterceptor` → `errorCatchingInterceptor` |
1245
- | User info missing from topbar | Add `...provideAuthLayoutIntegration()` to providers |
1246
- | Switcher shows all companies | Ensure user has company permissions assigned via `UserPermissionApiService` |
1247
- | Session not restored on refresh | 1) Use `guestGuard` on auth routes 2) Check refresh cookie in DevTools 3) Verify backend refresh endpoint |
1248
-
1249
- ---
1250
-
1251
- ## No-IAM Mode (Bypassing Permission Guards)
1252
-
1253
- When not using `@flusys/ng-iam`, routes with `permissionGuard` will fail (no permissions loaded).
1254
-
1255
- **Solution:** Set wildcard permissions after login:
1256
-
1257
- ```typescript
1258
- // Option 1: In login flow (recommended)
1259
- this.permissionValidator.setPermissions(['*']);
1260
-
1261
- // Option 2: In APP_INITIALIZER
1262
- { provide: APP_INITIALIZER, useFactory: (pv: PermissionValidatorService) => () => pv.setPermissions(['*']), deps: [PermissionValidatorService], multi: true }
211
+ User logs in
212
+ │
213
+ ├─ requiresSelection: false ──► Navigate to app
214
+ │
215
+ └─ requiresSelection: true
216
+ │
217
+ ├─ sessionId + companies[] returned
218
+ └─ Redirect to /auth/select-company
219
+ │
220
+ └─ User picks company/branch
221
+ │
222
+ └─ POST /auth/select { sessionId, companyId, branchId }
223
+ │
224
+ └─ Tokens + company data → Navigate to app
1263
225
  ```
1264
226
 
1265
- | Pattern | Meaning |
1266
- |---------|---------|
1267
- | `*` | Full access to everything |
1268
- | `user.*` | All user permissions |
1269
- | `branch.*` | All branch permissions |
1270
-
1271
227
  ---
1272
228
 
1273
- ## Extension System
1274
-
1275
- ng-auth provides injection tokens for extending registration, user list, and profile without modifying the core package.
1276
-
1277
- ### Extension Injection Tokens
1278
-
1279
- ```typescript
1280
- import {
1281
- AUTH_VALIDATION_CONFIG,
1282
- AUTH_REGISTRATION_EXTENSION,
1283
- AUTH_USER_LIST_EXTENSION,
1284
- AUTH_USER_DETAIL_CONFIG,
1285
- AUTH_PROFILE_EXTENSION,
1286
- AUTH_USER_FORM_EXTENSION,
1287
- } from '@flusys/ng-auth';
1288
- ```
1289
-
1290
- ### AUTH_VALIDATION_CONFIG
1291
-
1292
- Customize email and password validation.
229
+ ## Auth State Service
1293
230
 
1294
- | Property | Options |
1295
- |----------|---------|
1296
- | `email.pattern` | Custom regex |
1297
- | `email.message` | Error message |
1298
- | `password.minLength/maxLength` | Length constraints |
1299
- | `password.requireUppercase/Lowercase/Numbers/SpecialChars` | Character requirements |
1300
- | `password.pattern` | Custom regex |
1301
- | `password.message` | Error message |
231
+ `AuthStateService` is the single source of truth for authentication state, powered by Angular signals.
1302
232
 
1303
233
  ```typescript
1304
- { provide: AUTH_VALIDATION_CONFIG, useValue: { password: { minLength: 8, requireUppercase: true } } }
1305
-
1306
- // Helper functions
1307
- import { validatePassword, validateEmail } from '@flusys/ng-auth';
1308
- ```
1309
-
1310
- ### AUTH_REGISTRATION_EXTENSION
1311
-
1312
- Add extra fields to registration form.
1313
-
1314
- **IRegistrationExtraField:** `key`, `label`, `type` (text|number|select|checkbox|textarea|date|template), `placeholder?`, `required?`, `defaultValue?`, `options?`, `pattern?`, `validationMessage?`, `order?`, `containerClass?`, `template?`, `component?`
234
+ import { AuthStateService } from '@flusys/ng-auth';
1315
235
 
1316
- **IRegistrationExtensionProvider Methods:**
236
+ @Component({ ... })
237
+ export class MyComponent {
238
+ private authState = inject(AuthStateService);
1317
239
 
1318
- | Method | Purpose |
1319
- |--------|---------|
1320
- | `getExtraFields()` | Return field definitions |
1321
- | `transformRegistrationData?(data)` | Transform before submission (e.g., wrap in `additionalFields`) |
1322
- | `validateExtraFields?(data)` | Custom validation, return error message or null |
240
+ // Signals (read-only)
241
+ user = this.authState.user; // Signal<ICurrentUser | null>
242
+ company = this.authState.company; // Signal<ICompany | null>
243
+ branch = this.authState.branch; // Signal<IBranch | null>
244
+ isAuthenticated = this.authState.isAuthenticated; // Signal<boolean>
245
+ accessToken = this.authState.accessToken; // Signal<string | null>
1323
246
 
1324
- ```typescript
1325
- { provide: AUTH_REGISTRATION_EXTENSION, useClass: MyExtension }
1326
- // Custom component: inject(REGISTRATION_FIELD_CONTEXT) for field, value, updateValue()
247
+ // Computed
248
+ userName = computed(() => this.authState.user()?.name ?? 'Guest');
249
+ }
1327
250
  ```
1328
251
 
1329
- ### AUTH_USER_LIST_EXTENSION
1330
-
1331
- Add extra columns and actions to user list.
252
+ **AuthStateService API:**
1332
253
 
1333
- **IUserListExtraColumn:** `key`, `header`, `field?` (dot notation: `additionalFields.employeeId`), `template?`, `styleClass?`, `width?`, `sortable?`, `order?`
254
+ | Member | Type | Description |
255
+ |--------|------|-------------|
256
+ | `user` | `Signal<ICurrentUser \| null>` | Current authenticated user |
257
+ | `company` | `Signal<ICompany \| null>` | Selected company |
258
+ | `branch` | `Signal<IBranch \| null>` | Selected branch |
259
+ | `isAuthenticated` | `Signal<boolean>` | True if user is logged in |
260
+ | `accessToken` | `Signal<string \| null>` | In-memory JWT access token |
261
+ | `setUser(user)` | `void` | Update user state |
262
+ | `setTokens(tokens)` | `void` | Update access token |
263
+ | `setCompany(company, branch)` | `void` | Update company/branch |
264
+ | `clearAll()` | `void` | Clear all auth state (logout) |
1334
265
 
1335
- **IUserListExtraAction:** `key`, `label`, `icon`, `severity?`, `permission?`, `onClick(user)`, `isVisible?(user)`, `isDisabled?(user)`, `order?`
266
+ ---
1336
267
 
1337
- **IUserListExtensionProvider Methods:**
268
+ ## Auth API Service
1338
269
 
1339
- | Method | Purpose |
1340
- |--------|---------|
1341
- | `getExtraColumns()` | Return column definitions |
1342
- | `getExtraActions()` | Return action button definitions |
1343
- | `enrichUserData?(users)` | Transform user data (usually handled by backend enricher) |
270
+ `AuthApiService` handles all HTTP calls to the auth backend.
1344
271
 
1345
272
  ```typescript
1346
- { provide: AUTH_USER_LIST_EXTENSION, useFactory: (router, msg) => new MyExtension(router, msg), deps: [Router, MessageService] }
1347
- ```
1348
-
1349
- ### AUTH_USER_DETAIL_CONFIG
1350
-
1351
- Configure user detail view mode.
1352
-
1353
- **IUserDetailViewConfig:** `viewMode` (dialog|sidebar|page), `allowEdit?`, `width?`, `component?`, `tabs?` (array: key, label, icon?, component?, template?)
273
+ import { AuthApiService } from '@flusys/ng-auth';
1354
274
 
1355
- ```typescript
1356
- { provide: AUTH_USER_DETAIL_CONFIG, useValue: { viewMode: 'dialog', allowEdit: true, width: '800px' } }
275
+ @Component({ ... })
276
+ export class LoginPageComponent {
277
+ private authApi = inject(AuthApiService);
278
+
279
+ login(email: string, password: string): void {
280
+ this.authApi.login({ email, password }).subscribe(response => {
281
+ if (response.requiresSelection) {
282
+ // Redirect to company selection
283
+ }
284
+ // Navigate to app
285
+ });
286
+ }
287
+ }
1357
288
  ```
1358
289
 
1359
- ### AUTH_PROFILE_EXTENSION
290
+ **AuthApiService Methods:**
1360
291
 
1361
- Add extra fields that **users can edit** on their own profile page.
292
+ | Method | Endpoint | Description |
293
+ |--------|----------|-------------|
294
+ | `login(dto)` | `POST /auth/login` | Authenticate user |
295
+ | `register(dto)` | `POST /auth/register` | Self-register |
296
+ | `logout()` | `POST /auth/logout` | Invalidate tokens |
297
+ | `refresh()` | `POST /auth/refresh` | Refresh access token |
298
+ | `me()` | `GET /auth/me` | Get current user info |
299
+ | `selectCompany(dto)` | `POST /auth/select` | Select company after login |
300
+ | `verifyEmail(dto)` | `POST /auth/verify-email` | Verify OTP |
301
+ | `resendVerification()` | `POST /auth/resend-verification` | Resend OTP email |
302
+ | `changePassword(dto)` | `POST /auth/change-password` | Update password |
303
+ | `updateProfile(dto)` | `POST /auth/update-profile` | Update name/avatar |
1362
304
 
1363
- **IProfileExtraField:** `key`, `label`, `type` (text|number|select|checkbox|textarea|date|template), `placeholder?`, `required?`, `options?`, `pattern?`, `validationMessage?`, `order?`, `sectionKey?`, `template?`, `component?`, `injector?`
305
+ ---
1364
306
 
1365
- **IProfileExtraSection:** `key`, `title`, `icon?`, `order?`, `component?`, `template?`, `collapsible?`, `collapsed?`, `editPermission?`
307
+ ## HTTP Interceptors
1366
308
 
1367
- **IProfileExtensionProvider Methods:**
309
+ ### authInterceptor
1368
310
 
1369
- | Method | Purpose |
1370
- |--------|---------|
1371
- | `getExtraFields?()` | Return field definitions (user-editable) |
1372
- | `getExtraSections?()` | Return section definitions |
1373
- | `getExtraFieldValues?(userId)` | Fetch values from custom API |
1374
- | `saveExtraFieldValues?(userId, data)` | Save values to custom API |
1375
- | `validateExtraFields?(data)` | Custom validation, return error message or null |
311
+ Attaches the in-memory access token as `Authorization: Bearer <token>` header to every outgoing request (except public endpoints).
1376
312
 
1377
313
  ```typescript
1378
- { provide: AUTH_PROFILE_EXTENSION, useClass: MyProfileExtension }
1379
- // Custom component: inject(PROFILE_FIELD_CONTEXT) for field, value, updateValue()
314
+ provideHttpClient(
315
+ withInterceptors([authInterceptor])
316
+ )
1380
317
  ```
1381
318
 
1382
- ### AUTH_USER_FORM_EXTENSION
1383
-
1384
- Add extra fields that **admins can edit** on the user management form (`/administration/users/:id`).
1385
- These fields appear as **view-only** on the user's profile page with "Managed by Admin" label.
1386
-
1387
- **IUserFormExtraField:** `key`, `label`, `type` (text|number|select|checkbox|textarea|date|template), `placeholder?`, `required?`, `options?`, `pattern?`, `validationMessage?`, `order?`, `template?`, `component?`, `injector?`
319
+ ### tokenRefreshInterceptor
1388
320
 
1389
- **IUserFormExtensionProvider Methods:**
321
+ On 401 responses, automatically:
322
+ 1. Calls `POST /auth/refresh`
323
+ 2. Updates `AuthStateService` with new token
324
+ 3. Retries the original failed request
1390
325
 
1391
- | Method | Purpose |
1392
- |--------|---------|
1393
- | `getExtraFields?()` | Return field definitions (admin-editable) |
1394
- | `getExtraFieldValues?(userId)` | Fetch values from custom API |
1395
- | `saveExtraFieldValues?(userId, data)` | Save values to custom API |
1396
- | `validateExtraFields?(data)` | Custom validation, return error message or null |
326
+ All concurrent 401s are queued — only one refresh call is made.
1397
327
 
1398
328
  ```typescript
1399
- { provide: AUTH_USER_FORM_EXTENSION, useClass: MyUserFormExtension }
1400
- // Custom component: inject(USER_FORM_FIELD_CONTEXT) for field, value, updateValue()
329
+ provideHttpClient(
330
+ withInterceptors([tokenRefreshInterceptor])
331
+ )
1401
332
  ```
1402
333
 
1403
- **Profile vs Admin Form Pattern:**
1404
-
1405
- | Provider | Used In | Who Edits | Profile Display |
1406
- |----------|---------|-----------|-----------------|
1407
- | `AUTH_PROFILE_EXTENSION` | Profile page | User | Editable fields |
1408
- | `AUTH_USER_FORM_EXTENSION` | Admin user form | Admin | View-only ("Managed by Admin") |
1409
-
1410
- ### Provider Factory Example
1411
-
1412
- Combine all extensions in a single provider function for app.config.ts:
334
+ **Interceptor Order Matters:**
1413
335
 
1414
336
  ```typescript
1415
- export function provideAuthExtensions(): Provider[] {
1416
- return [
1417
- { provide: AUTH_VALIDATION_CONFIG, useValue: myValidationConfig },
1418
- { provide: AUTH_REGISTRATION_EXTENSION, useClass: MyRegistrationExtension },
1419
- { provide: AUTH_USER_LIST_EXTENSION, useFactory: (r, m) => new MyUserListExtension(r, m), deps: [Router, MessageService] },
1420
- { provide: AUTH_USER_DETAIL_CONFIG, useValue: { viewMode: 'dialog', allowEdit: true } },
1421
- { provide: AUTH_PROFILE_EXTENSION, useClass: MyProfileExtension }, // User-editable fields
1422
- { provide: AUTH_USER_FORM_EXTENSION, useClass: MyUserFormExtension }, // Admin-editable fields
1423
- ];
1424
- }
1425
- // Usage: providers: [...provideAuthExtensions()]
337
+ withInterceptors([
338
+ authInterceptor, // Must be FIRST (attaches token)
339
+ tokenRefreshInterceptor, // Must be SECOND (handles 401)
340
+ ])
1426
341
  ```
1427
342
 
1428
- ### additionalFields Flow
1429
-
1430
- When using extensions, registration data flows as:
1431
-
1432
- 1. **Frontend:** `transformRegistrationData()` converts extra fields to `additionalFields`
1433
- 2. **Backend:** Registration stores `additionalFields` in PendingRegistration (email flow) or passes to `USER_ENRICHER.onUserCreated`
1434
- 3. **Backend:** `USER_ENRICHER.onUserCreated(userId, additionalFields, queryRunner)` creates related entities (e.g., UserProfileInfo) within the same transaction
1435
- 4. **Error handling:** If `onUserCreated` throws, the entire registration rolls back
1436
-
1437
343
  ---
1438
344
 
1439
- ## Translation System (v4.0.0+)
1440
-
1441
- ### Smart Translation Pattern with Fallback
345
+ ## Guards
1442
346
 
1443
- All text fields (labels, placeholders, headers, titles) now support intelligent translation fallback:
347
+ | Guard | Usage | Behavior |
348
+ |-------|-------|---------|
349
+ | `appInitGuard` | `canActivate` | Restores session from cookie on page refresh |
350
+ | `AuthenticatedGuard` | `canActivate` | Redirects to `/auth/login` if not authenticated |
351
+ | `GuestGuard` | `canActivate` | Redirects to `/` if already authenticated |
352
+ | `CompanySelectedGuard` | `canActivate` | Redirects to `/auth/select-company` if company not chosen |
1444
353
 
1445
354
  ```typescript
1446
- // Interface Pattern
1447
- interface IField {
1448
- label: string; // Fallback text (always required)
1449
- labelKey?: string; // Translation key (optional)
1450
- placeholder?: string; // Fallback text (optional)
1451
- placeholderKey?: string; // Translation key (optional)
355
+ // Route with all guards
356
+ {
357
+ path: '',
358
+ canActivate: [appInitGuard],
359
+ children: [
360
+ {
361
+ path: 'dashboard',
362
+ canActivate: [AuthenticatedGuard, CompanySelectedGuard],
363
+ loadComponent: () => import('./dashboard.component'),
364
+ },
365
+ ],
1452
366
  }
1453
367
  ```
1454
368
 
1455
- ### Message/Validation Parameter Support
369
+ ---
1456
370
 
1457
- Validation messages and custom messages now support parameter interpolation:
371
+ ## Pages
1458
372
 
1459
- ```typescript
1460
- // Interface Pattern
1461
- interface IValidationConfig {
1462
- message?: string; // Fallback message
1463
- messageKey?: string; // Translation key
1464
- messageParam?: Record<string, string | number>; // Interpolation params
1465
- }
373
+ `ng-auth` exports pre-built page components routable via `AUTH_ROUTES`:
1466
374
 
1467
- interface IField {
1468
- validationMessage?: string;
1469
- validationMessageKey?: string;
1470
- validationMessageParam?: Record<string, string | number>;
1471
- }
1472
- ```
1473
-
1474
- **Usage Example:**
1475
375
  ```typescript
1476
- // Provider configuration
1477
- validationConfig: {
1478
- message: "Password must be at least {{min}} characters",
1479
- messageKey: "auth.validation.password.min.length",
1480
- messageParam: { min: 8 }
1481
- }
376
+ import { AUTH_ROUTES } from '@flusys/ng-auth';
1482
377
 
1483
- // Field configuration
1484
- {
1485
- label: "Company Name",
1486
- labelKey: "admin.company.name",
1487
- placeholder: "Enter company name",
1488
- placeholderKey: "admin.company.name.placeholder",
1489
- validationMessage: "Company name is required",
1490
- validationMessageKey: "admin.company.name.required",
1491
- validationMessageParam: {}
1492
- }
378
+ // app.routes.ts
379
+ { path: 'auth', loadChildren: () => AUTH_ROUTES }
1493
380
  ```
1494
381
 
1495
- ### safeTranslate() Method
382
+ | Route | Component | Description |
383
+ |-------|-----------|-------------|
384
+ | `/auth/login` | `LoginPageComponent` | Email + password login form |
385
+ | `/auth/register` | `RegisterPageComponent` | Self-registration form |
386
+ | `/auth/verify-email` | `VerifyEmailPageComponent` | OTP verification |
387
+ | `/auth/select-company` | `SelectCompanyPageComponent` | Company/branch picker |
388
+ | `/auth/profile` | `ProfilePageComponent` | User profile editor |
1496
389
 
1497
- All form components have a protected `safeTranslate()` helper method that intelligently falls back when translations are missing:
390
+ All pages support full i18n via `TranslatePipe`.
1498
391
 
1499
- ```typescript
1500
- /** Translate with fallback: if key returns unchanged, use fallback instead */
1501
- safeTranslate(key: string | undefined, fallback: string): string {
1502
- if (!key) return fallback;
1503
- const translated = this.translate(key);
1504
- return translated === key ? fallback : translated; // Smart fallback!
1505
- }
1506
- ```
392
+ ---
1507
393
 
1508
- **Behavior:**
1509
- ```
1510
- safeTranslate(undefined, "Name")
1511
- → Returns: "Name" (no key, use fallback)
394
+ ## Provider Adapters
1512
395
 
1513
- safeTranslate("auth.profile.name", "Name")
1514
- → Returns: "Nome" (translated)
396
+ ### AUTH_EMAIL_PROVIDER
1515
397
 
1516
- safeTranslate("auth.new.feature", "Name")
1517
- → Returns: "Name" (no translation, use fallback)
1518
- ```
398
+ `ng-auth` requires an email adapter for verification emails. Register this in your NestJS backend (not Angular). On the Angular side, this is handled automatically by the backend.
1519
399
 
1520
- ### Template Usage Patterns
400
+ ### User Enricher
1521
401
 
1522
- **Pattern 1: Simple Label**
1523
- ```html
1524
- {{ safeTranslate(field.labelKey, field.label) }}
1525
- ```
1526
-
1527
- **Pattern 2: Placeholder with Default**
1528
- ```html
1529
- {{ safeTranslate(field.placeholderKey, field.placeholder ?? '') }}
1530
- ```
402
+ You can inject additional user data at login time:
1531
403
 
1532
- **Pattern 3: Fallback Translation Chain**
1533
- ```html
1534
- {{ safeTranslate(field.placeholderKey, field.placeholder ?? ('shared.select' | translate)) }}
1535
- ```
404
+ ```typescript
405
+ // Custom enricher (rare — usually not needed)
406
+ import { USER_ENRICHER } from '@flusys/ng-auth';
1536
407
 
1537
- **Pattern 4: Complex Nested Fallback**
1538
- ```html
1539
- {{ safeTranslate(
1540
- field.placeholderKey,
1541
- field.placeholder ?? safeTranslate(field.labelKey, field.label)
1542
- ) }}
408
+ providers: [
409
+ {
410
+ provide: USER_ENRICHER,
411
+ useClass: MyUserEnricher,
412
+ },
413
+ ]
1543
414
  ```
1544
415
 
1545
- ### Components Using safeTranslate()
416
+ ---
1546
417
 
1547
- - **RegisterPageComponent** - Registration extra fields
1548
- - **ProfilePageComponent** - User editable + admin view-only fields
1549
- - **UserFormComponent** - Admin user form fields
1550
- - **UserListComponent** - Custom columns and action labels
418
+ ## API Endpoints
419
+
420
+ | Method | Endpoint | Auth | Description |
421
+ |--------|----------|------|-------------|
422
+ | POST | `/auth/login` | Public | Login with email/password |
423
+ | POST | `/auth/register` | Public | Self-registration |
424
+ | POST | `/auth/logout` | Bearer | Logout and clear cookie |
425
+ | POST | `/auth/refresh` | Cookie | Refresh access token |
426
+ | GET | `/auth/me` | Bearer | Get current user |
427
+ | POST | `/auth/select` | Session | Select company after login |
428
+ | POST | `/auth/verify-email` | Bearer | Verify email OTP |
429
+ | POST | `/auth/resend-verification` | Bearer | Resend OTP |
430
+ | POST | `/auth/change-password` | Bearer | Change password |
431
+ | POST | `/auth/update-profile` | Bearer | Update profile |
1551
432
 
1552
- ### Translation File Integration
433
+ ---
1553
434
 
1554
- When translations exist, they are automatically used:
435
+ ## Configuration Reference
1555
436
 
1556
- **Translation file (en.json):**
1557
- ```json
1558
- {
1559
- "auth.profile.name": "User Name",
1560
- "auth.phone.placeholder": "Enter phone number",
1561
- "admin.company.name": "Company",
1562
- "shared.select": "Select an option"
1563
- }
1564
- ```
437
+ | Config Key | Type | Default | Description |
438
+ |------------|------|---------|-------------|
439
+ | `services.auth.enabled` | `boolean` | `false` | Enable auth module |
440
+ | `services.auth.features.signUp` | `boolean` | `false` | Allow self-registration |
441
+ | `services.auth.features.emailVerification` | `boolean` | `false` | Require email OTP |
442
+ | `services.auth.features.companySelection` | `boolean` | `false` | Enable multi-company |
1565
443
 
1566
- **When key exists in translations:** Shows translated value
1567
- **When key missing from translations:** Shows fallback text (label/placeholder/message)
1568
- **When key undefined:** Shows fallback text directly
444
+ ---
1569
445
 
1570
- ### Corner Cases Handled
446
+ ## Troubleshooting
1571
447
 
1572
- | Scenario | Result |
1573
- |----------|--------|
1574
- | Key undefined | Returns fallback ✅ |
1575
- | Translation missing | Returns fallback ✅ |
1576
- | No TRANSLATE_ADAPTER | Returns fallback ✅ |
1577
- | Fallback is empty string | Displays empty string (valid) ✅ |
1578
- | Parameter interpolation | Supported via messageParam ✅ |
1579
- | Nested safeTranslate calls | Works correctly ✅ |
448
+ **Token refresh loop (endless 401 cycle)**
1580
449
 
1581
- ### Migration from v3.0.x
450
+ This happens when the refresh endpoint itself returns 401. Ensure the refresh token cookie hasn't expired. Also verify that `tokenRefreshInterceptor` skips the `/auth/refresh` endpoint:
1582
451
 
1583
- **Removed fields** (no longer needed):
1584
452
  ```typescript
1585
- ❌ labelFallback → Use label directly
1586
- ❌ placeholderFallback → Use placeholder directly
1587
- ❌ titleFallback → Use title directly
1588
- ❌ headerFallback → Use header directly
1589
- ❌ messageFallback → Use message directly
453
+ // The interceptor automatically skips /auth/refresh to prevent loops
1590
454
  ```
1591
455
 
1592
- **Removed:** `labelFallback`, `placeholderFallback`, `titleFallback`, `headerFallback`, `messageFallback`
456
+ **`appInitGuard` redirects to login on every refresh**
1593
457
 
1594
- **Added:** `messageParam`, `validationMessageParam` for parameter interpolation
458
+ Ensure your backend sets the refresh token cookie with `httpOnly: true`, `sameSite: 'lax'`, and the correct `domain`/`path`. The browser must send the cookie automatically with `POST /auth/refresh`.
1595
459
 
1596
- **No breaking changes:** Existing implementations still work due to smart fallback logic.
460
+ **Company selection page shown even after selecting**
1597
461
 
1598
- ---
462
+ Verify that `POST /auth/select` returns both tokens AND the company/branch data. `AuthStateService.setCompany()` must be called with the response.
1599
463
 
1600
- ## See Also
464
+ **User profile picture doesn't load**
1601
465
 
1602
- - [CORE-GUIDE.md](./CORE-GUIDE.md) - APP_CONFIG configuration
1603
- - [SHARED-GUIDE.md](./SHARED-GUIDE.md) - Provider interfaces
1604
- - [LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md) - Layout integration
1605
- - [IAM-GUIDE.md](./IAM-GUIDE.md) - Permission management
1606
- - [../FLUSYS_NEST/docs/AUTH-GUIDE.md](../../FLUSYS_NEST/docs/AUTH-GUIDE.md) - Backend auth module
466
+ Profile pictures are fetched via `FileUrlService`, not by constructing URLs. Ensure `ng-storage` providers are registered and `services.storage.enabled: true`.
1607
467
 
1608
- ---
468
+ **Login form validation errors not translated**
1609
469
 
1610
- **Last Updated:** 2026-03-03
1611
- **Version:** 4.0.0-rc
1612
- **Angular Version:** 21
470
+ Provide `TRANSLATE_ADAPTER` via `provideLocalization()`. The fallback text (non-translated) is always shown if the adapter is absent.
1613
471
 
1614
- ## Key Changes in v4.0.0-rc
472
+ ---
1615
473
 
1616
- - ✅ Smart translation fallback system with `safeTranslate()` helper
1617
- - ✅ Message parameter support (`messageParam`, `validationMessageParam`)
1618
- - ✅ Removed redundant `*Fallback` interface fields
1619
- - ✅ All 4 form pages use safe translation pattern
1620
- - ✅ Type-safe, TypeScript enforced
474
+ ## License
1621
475
 
1622
- All 15 components use responsive design and dark mode support. Form components use Signal Forms.
476
+ MIT © FLUSYS