@flusys/ng-auth 1.1.0 → 1.2.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1158 +1,152 @@
1
- # @flusys/ng-auth Package Guide
1
+ # @flusys/ng-auth
2
2
 
3
- ## Overview
3
+ JWT authentication library for FLUSYS Angular apps — login, session restore, token refresh, multi-company selection, and provider adapter registration.
4
4
 
5
- `@flusys/ng-auth` provides complete authentication functionality including login, registration, token management, company/branch selection, user administration, and permission handling.
6
-
7
- **Package:** `@flusys/ng-auth`
8
- **Version:** 1.1.0
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 (handles 401 + token 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
- ├─────────────────────────────────────────────────────────────┤
46
- │ Constants │
47
- │ └── AUTH_ENDPOINTS (endpoint path definitions) │
48
- └─────────────────────────────────────────────────────────────┘
49
- ```
50
-
51
- ---
52
-
53
- ## Services
54
-
55
- ### AuthApiService
56
-
57
- HTTP client for authentication endpoints.
58
-
59
- ```typescript
60
- import { AuthApiService } from '@flusys/ng-auth';
61
-
62
- const authApi = inject(AuthApiService);
63
-
64
- // Authentication
65
- authApi.login({ email, password }); // POST /auth/login
66
- authApi.register({ name, email, password }); // POST /auth/register
67
- authApi.refresh(); // POST /auth/refresh
68
- authApi.logout(); // POST /auth/logout
69
- authApi.me(); // GET /auth/me
70
-
71
- // Company/Branch selection
72
- authApi.select({ companyId, branchId, sessionId }); // POST /auth/select
73
- authApi.switchCompany({ companyId, branchId }); // POST /auth/switch-company
74
-
75
- // Password management
76
- authApi.changePassword({ currentPassword, newPassword });
77
- authApi.forgotPassword({ email });
78
- authApi.resetPassword({ token, newPassword });
79
- ```
80
-
81
- ### AuthStateService
82
-
83
- Signal-based centralized auth state management with auto-persistence.
84
-
85
- ```typescript
86
- import { AuthStateService } from '@flusys/ng-auth';
87
-
88
- const authState = inject(AuthStateService);
89
-
90
- // Read-only signals
91
- authState.user(); // Signal<IUserInfo | null>
92
- authState.accessToken(); // Signal<string | null>
93
- authState.company(); // Signal<ICompanyInfo | null>
94
- authState.branch(); // Signal<IBranchInfo | null>
95
- authState.availableCompanies(); // Signal<ICompanyWithBranches[]>
96
- authState.selectionSessionId(); // Signal<string | null>
97
-
98
- // Computed signals
99
- authState.isAuthenticated(); // Signal<boolean>
100
- authState.requiresCompanySelection(); // Signal<boolean>
101
- authState.userName(); // Signal<string>
102
- authState.companyName(); // Signal<string>
103
- authState.branchName(); // Signal<string>
104
- authState.tenantId(); // Signal<string | null>
105
-
106
- // State mutations
107
- authState.setLoginState(response); // After successful login
108
- authState.setSelectionState(response); // After company/branch selection
109
- authState.clearSelectionState(); // Clear pending selection
110
- authState.resetState(); // Full logout
111
- authState.navigateToLogin(returnUrl?);
112
- authState.navigateAfterLogin();
113
- ```
114
-
115
- **Auto-persistence:** Tokens and company/branch IDs are automatically saved to localStorage via effects.
116
-
117
- ### AuthInitService
118
-
119
- Handles session restoration on page refresh.
120
-
121
- ```typescript
122
- import { AuthInitService } from '@flusys/ng-auth';
123
-
124
- const authInit = inject(AuthInitService);
125
-
126
- // Signals
127
- authInit.initialized(); // Signal<boolean>
128
- authInit.loading(); // Signal<boolean>
129
- authInit.error(); // Signal<string | null>
130
-
131
- // Restore session (called by appInitGuard)
132
- authInit.initialize().subscribe({
133
- next: (user) => console.log('Session restored:', user),
134
- error: (err) => console.log('No session or expired')
135
- });
136
- ```
137
-
138
- **Flow:**
139
- 1. Attempts `POST /auth/refresh` with httpOnly cookie
140
- 2. On success, calls `GET /auth/me` to get user info
141
- 3. Updates AuthStateService with restored session
142
- 4. Caches result to prevent duplicate calls
143
-
144
- ### Resource API Services
145
-
146
- All extend `ApiResourceService` from ng-shared with signal-based state.
147
-
148
- ```typescript
149
- import { UserApiService, CompanyApiService, BranchApiService } from '@flusys/ng-auth';
150
-
151
- // Common signals (from ApiResourceService)
152
- service.data(); // Signal<T[]>
153
- service.isLoading(); // Signal<boolean>
154
- service.error(); // Signal<string | null>
155
-
156
- // Common methods
157
- await service.insertAsync(data); // Create
158
- await service.updateAsync(data); // Update
159
- await service.deleteAsync(id); // Delete
160
- await service.fetchByIdAsync(id); // Get one
161
- service.fetchList(search, options); // Get list with pagination
162
- ```
163
-
164
- **UserApiService** - Endpoint: `administration/users`
165
- ```typescript
166
- userApi.verifyEmail(userId);
167
- userApi.verifyPhone(userId);
168
- userApi.updateStatus(userId, status);
169
- userApi.updateProfile(data);
170
- ```
171
-
172
- **CompanyApiService** - Endpoint: `administration/company`
173
-
174
- **BranchApiService** - Endpoint: `administration/branch`
175
- ```typescript
176
- branchApi.currentCompanyId(); // Signal for company context
177
- branchApi.companyBranches(); // Computed: branches filtered by company
178
- branchApi.setCurrentCompanyId(id);
179
- branchApi.insertBranch(data); // Auto-sets companyId
180
- branchApi.fetchByCompany(companyId);
181
- ```
182
-
183
- ### UserPermissionApiService
184
-
185
- Manages user-to-company and user-to-branch permission assignments.
186
-
187
- ```typescript
188
- import { UserPermissionApiService } from '@flusys/ng-auth';
189
-
190
- const permApi = inject(UserPermissionApiService);
191
-
192
- // Company permissions
193
- permApi.assignUserCompanies({
194
- userId: 'user-1',
195
- items: [
196
- { targetId: 'company-1', isAdd: true }, // Assign
197
- { targetId: 'company-2', isAdd: false } // Revoke
198
- ]
199
- });
200
- permApi.getUserCompanies(userId); // Get user's companies
201
- permApi.assignUserToCompany(userId, companyId); // Single assign
202
- permApi.revokeUserFromCompany(userId, companyId); // Single revoke
203
-
204
- // Branch permissions
205
- permApi.assignUserBranches({
206
- userId: 'user-1',
207
- items: [
208
- { targetId: 'branch-1', isAdd: true }
209
- ]
210
- });
211
- permApi.getUserBranches(userId); // Get user's branches
212
- permApi.assignUserToBranch(userId, branchId);
213
- permApi.revokeUserFromBranch(userId, branchId);
214
- ```
215
-
216
- **Backend Endpoints:**
217
- - `POST /administration/permissions/user-company/assign`
218
- - `GET /administration/permissions/user-company?userId=xxx`
219
- - `POST /administration/permissions/user-branch/assign`
220
- - `GET /administration/permissions/user-branch?userId=xxx`
221
-
222
- ---
223
-
224
- ## Guards
225
-
226
- ### authGuard
227
-
228
- Protects routes requiring authentication.
229
-
230
- ```typescript
231
- import { authGuard } from '@flusys/ng-auth';
232
-
233
- {
234
- path: 'dashboard',
235
- canActivate: [authGuard],
236
- loadComponent: () => import('./dashboard.component')
237
- }
238
- ```
239
-
240
- **Checks:**
241
- 1. User is authenticated
242
- 2. If company feature enabled: company AND branch are selected
243
- 3. Redirects to `/auth/login` with `returnUrl` if checks fail
244
-
245
- ### guestGuard
246
-
247
- Protects auth routes (login, register) from authenticated users.
248
-
249
- ```typescript
250
- import { guestGuard } from '@flusys/ng-auth';
251
-
252
- {
253
- path: 'auth/login',
254
- canActivate: [guestGuard],
255
- loadComponent: () => import('./login.component')
256
- }
257
- ```
258
-
259
- **Behavior:**
260
- 1. Restores session via `AuthInitService.initialize()` first
261
- 2. If authenticated AND company selected: redirects to home or returnUrl
262
- 3. If selection pending: allows access (login handles Step 2)
263
- 4. If not authenticated: allows access
264
-
265
- ### companyFeatureGuard
266
-
267
- Enables/disables company management routes based on config.
268
-
269
- ```typescript
270
- import { companyFeatureGuard } from '@flusys/ng-auth';
271
-
272
- {
273
- path: 'administration/company',
274
- canActivate: [authGuard, companyFeatureGuard],
275
- loadComponent: () => import('./company-list.component')
276
- }
277
- ```
278
-
279
- **Checks:** `enableCompanyFeature` in APP_CONFIG. Redirects to `/` if disabled.
280
-
281
- ### emailFeatureGuard
282
-
283
- Protects email-related routes (forgot-password, reset-password, verify-email).
284
-
285
- ```typescript
286
- import { emailFeatureGuard } from '@flusys/ng-auth';
287
-
288
- {
289
- path: 'auth/forgot-password',
290
- canActivate: [emailFeatureGuard, guestGuard],
291
- loadComponent: () => import('./forgot-password.component')
292
- }
293
- ```
294
-
295
- **Checks:** `services.email.enabled` in APP_CONFIG. Redirects to `/auth/login` if disabled.
296
-
297
- ### signUpFeatureGuard
298
-
299
- Protects registration route when sign-up is disabled.
300
-
301
- ```typescript
302
- import { signUpFeatureGuard } from '@flusys/ng-auth';
303
-
304
- {
305
- path: 'auth/register',
306
- canActivate: [signUpFeatureGuard, guestGuard],
307
- loadComponent: () => import('./register.component')
308
- }
309
- ```
310
-
311
- **Checks:** `enableSignUp` in APP_CONFIG (defaults to `true`). Redirects to `/auth/login` if `false`.
312
-
313
- ---
314
-
315
- ## Interceptors
316
-
317
- ### authInterceptor
318
-
319
- Adds authentication headers to HTTP requests.
320
-
321
- ```typescript
322
- import { authInterceptor } from '@flusys/ng-auth';
323
-
324
- provideHttpClient(
325
- withInterceptors([authInterceptor, ...])
326
- )
327
- ```
328
-
329
- **Adds:**
330
- - `Authorization: Bearer {token}` if token exists
331
- - Tenant header if multi-tenant enabled
332
- - `withCredentials: true` for auth endpoints (cookie support)
333
-
334
- ### tokenRefreshInterceptor
335
-
336
- Handles 401 responses and token refresh.
337
-
338
- ```typescript
339
- import { tokenRefreshInterceptor } from '@flusys/ng-auth';
340
-
341
- provideHttpClient(
342
- withInterceptors([authInterceptor, tokenRefreshInterceptor, ...])
343
- )
344
- ```
345
-
346
- **Behavior:**
347
- 1. Intercepts 401 Unauthorized responses
348
- 2. Skips refresh for: `/auth/refresh`, `/auth/login`, `/auth/register`, `/auth/select`
349
- 3. Uses `TokenRefreshStateService` to queue concurrent requests during refresh
350
- 4. On success: updates token, retries original request
351
- 5. On failure: logs out, shows error, redirects to login
5
+ [![npm version](https://img.shields.io/npm/v/@flusys/ng-auth.svg)](https://www.npmjs.com/package/@flusys/ng-auth)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
352
7
 
353
8
  ---
354
9
 
355
- ## Constants
356
-
357
- ### AUTH_ENDPOINTS
358
-
359
- Centralized endpoint definitions used by interceptors.
360
-
361
- ```typescript
362
- import { AUTH_ENDPOINTS, isEndpoint } from '@flusys/ng-auth';
363
-
364
- // Endpoint constants
365
- AUTH_ENDPOINTS.LOGIN // '/auth/login'
366
- AUTH_ENDPOINTS.LOGOUT // '/auth/logout'
367
- AUTH_ENDPOINTS.REFRESH // '/auth/refresh'
368
- AUTH_ENDPOINTS.REGISTER // '/auth/register'
369
- AUTH_ENDPOINTS.SELECT // '/auth/select'
370
- AUTH_ENDPOINTS.SWITCH_COMPANY // '/auth/switch-company'
371
-
372
- // Helper function
373
- isEndpoint(url, endpoint); // Check if URL matches specific endpoint
374
- ```
375
-
376
- ---
377
-
378
- ## Provider Adapters
379
-
380
- ng-auth implements provider interfaces from ng-shared, enabling ng-iam and ng-storage to access auth data without direct dependencies.
381
-
382
- ### provideAuthProviders()
383
-
384
- Registers provider adapters for ng-iam/ng-storage.
10
+ ## Installation
385
11
 
386
- ```typescript
387
- import { provideAuthProviders } from '@flusys/ng-auth';
388
-
389
- // app.config.ts
390
- providers: [
391
- ...provideAuthProviders()
392
- ]
12
+ ```bash
13
+ npm install @flusys/ng-auth @flusys/ng-core @flusys/ng-shared
393
14
  ```
394
15
 
395
- **Registers:**
396
- - `USER_PROVIDER` → `AuthUserProviderAdapter` (user list for ng-iam)
397
- - `COMPANY_API_PROVIDER` → `AuthCompanyApiProviderAdapter` (company list for ng-iam)
398
- - `USER_PERMISSION_PROVIDER` → `AuthUserPermissionProviderAdapter` (permission queries)
399
-
400
- ### provideAuthLayoutIntegration()
401
-
402
- Bridges ng-auth to ng-layout components.
16
+ ## 1. Register Providers
403
17
 
404
18
  ```typescript
405
- import { provideAuthLayoutIntegration } from '@flusys/ng-auth';
406
-
407
19
  // app.config.ts
408
- providers: [
409
- ...provideAuthLayoutIntegration()
410
- ]
411
- ```
412
-
413
- **Registers:**
414
- - `LAYOUT_AUTH_STATE` → `AuthLayoutStateAdapter` (user/company/branch signals)
415
- - `LAYOUT_AUTH_API` → `AuthLayoutApiAdapter` (logout, switch company, etc.)
416
-
417
- **AuthLayoutApiAdapter Methods:**
418
- ```typescript
419
- logOut();
420
- navigateLogin(withUrl?: boolean);
421
- switchCompany(companyId, branchId); // Triggers page reload
422
- getUserCompanies(); // User's permitted companies
423
- getCompanyBranches(companyId); // User's permitted branches
424
- ```
425
-
426
- ---
427
-
428
- ## Pages
429
-
430
- All page components follow Angular 21 best practices:
431
- - `ChangeDetectionStrategy.OnPush` for optimal performance
432
- - Signal Forms (`@angular/forms/signals`) with validation
433
- - 100% responsive design (mobile-first with Tailwind)
434
- - Dark mode compatible (PrimeNG + Tailwind CSS variables)
435
- - `inject()` for dependency injection
436
-
437
- ### Authentication Pages
438
-
439
- **LoginPageComponent** (`/auth/login`)
440
- - Multi-step login: Step 1 = credentials, Step 2 = company/branch selection (if needed)
441
- - Visual step indicator (when company feature enabled)
442
- - Email verification step (when email not verified)
443
- - Auto-selects if only one company/branch available
444
- - Stores returnUrl for post-login redirect
445
- - Responsive form layout with mobile padding
446
- - **Conditional UI based on config:**
447
- - "Forgot password?" link hidden when `services.email.enabled: false`
448
- - "Sign Up" link hidden when `enableSignUp: false`
449
-
450
- **RegisterPageComponent** (`/auth/register`)
451
- - User registration with optional company creation
452
- - Two-column responsive grid layout
453
- - Email/password validation with signal forms
454
- - Company details section (when company feature enabled)
455
- - **Default company/branch support:**
456
- - When `defaultCompanySlug` is set: pre-fills company code field and makes it readonly
457
- - When `defaultBranchSlug` is set: pre-fills branch code field and makes it readonly
458
- - "Create new company" option hidden when defaults are configured
459
-
460
- **ForgotPasswordPageComponent** (`/auth/forgot-password`)
461
- - Email input with validation
462
- - Success state showing email sent confirmation
463
-
464
- **ResetPasswordPageComponent** (`/auth/reset-password`)
465
- - Password reset with token validation
466
- - Password confirmation matching
467
- - Success/invalid token states
468
-
469
- **VerifyEmailPageComponent** (`/auth/verify-email`)
470
- - Token verification with SSR support (TransferState)
471
- - Resend verification option
472
- - Success/failure states
473
-
474
- ### Profile Page
475
-
476
- **ProfilePageComponent** (`/profile`)
477
- - Page title with description
478
- - Avatar section with initials fallback
479
- - Update name and phone
480
- - Upload profile picture (requires `PROFILE_UPLOAD_PROVIDER` from ng-storage)
481
- - Change password section with strength indicator
482
- - View company/branch info (if company feature enabled)
483
- - View roles/actions (requires `PROFILE_PERMISSION_PROVIDER` from ng-iam)
484
- - Responsive two-column layout on desktop
485
-
486
- **Optional Provider Dependencies:**
487
-
488
- | Provider | Source | Feature Enabled |
489
- |----------|--------|-----------------|
490
- | `PROFILE_UPLOAD_PROVIDER` | ng-storage via `provideStorageProviders()` | Profile picture upload |
491
- | `PROFILE_PERMISSION_PROVIDER` | ng-iam via `provideIamProviders()` | Roles/actions display |
492
-
493
- **Graceful Degradation:**
494
- - If provider not registered, feature shows "not enabled" message
495
- - If service disabled in config, section is hidden
496
- - No errors thrown for missing optional providers
497
-
498
- ### Administration Pages
499
-
500
- **AdministrationPageComponent** (`/administration`)
501
- - Page title "Administration" with dynamic subtitle
502
- - Horizontal scrolling tabs (hidden scrollbar on mobile)
503
- - Tabs: Users, Companies, Branches
504
- - Company/Branch tabs only visible when company feature enabled
505
-
506
- **User Management:**
507
- - `UserListComponent` - LazyLoad datatable with Edit, Delete, Permission actions
508
- - `UserFormComponent` - Create/edit user with auto company/branch assignment
509
- - Permission dialogs for company/branch assignments
510
-
511
- **Company Management:**
512
- - `CompanyListComponent` - Company CRUD with View Branches action
513
- - `CompanyFormComponent` - Create/edit company with auto slug generation
514
-
515
- **Branch Management:**
516
- - `BranchListComponent` - Branch CRUD filtered by company dropdown
517
- - `BranchFormComponent` - Create/edit branch with company selection
518
-
519
- **Table Features (all list components):**
520
- - Responsive header (stacked on mobile)
521
- - Horizontal scroll wrapper (`overflow-x-auto`)
522
- - Conditional pagination (`[paginator]="total() > 0"`)
523
- - Striped rows (`p-datatable-striped`)
524
- - Lazy loading for performance
525
-
526
- ---
527
-
528
- ## Dialog Components
529
-
530
- Both dialog components use Angular 21 patterns:
531
- - `ChangeDetectionStrategy.OnPush`
532
- - `input.required<T>()` for required inputs
533
- - `output<void>()` for events
534
- - `effect()` for reactive data loading
535
- - Responsive breakpoints (`{ '960px': '90vw', '640px': '95vw' }`)
536
-
537
- ### UserCompanyPermissionDialogComponent
538
-
539
- Manages user-to-company assignments with checkboxes.
540
-
541
- ```typescript
542
- import { UserCompanyPermissionDialogComponent } from '@flusys/ng-auth';
543
-
544
- @Component({
545
- imports: [UserCompanyPermissionDialogComponent],
546
- template: `
547
- <lib-user-company-permission-dialog
548
- [visible]="showDialog()"
549
- [user]="selectedUser()"
550
- (closed)="showDialog.set(false)"
551
- (permissionsChanged)="reloadData()" />
552
- `
553
- })
554
- ```
555
-
556
- **Inputs/Outputs:**
557
- ```typescript
558
- readonly visible = input.required<boolean>();
559
- readonly user = input.required<IUser | null>();
560
- readonly closed = output<void>();
561
- readonly permissionsChanged = output<void>();
562
- ```
563
-
564
- **Features:**
565
- - Shows all companies with checkboxes
566
- - Pre-checks assigned companies
567
- - Immediate save on toggle (no Save button)
568
- - Toast notifications for success/error
569
- - Loading spinner during data fetch
570
- - Disabled checkboxes while saving
571
-
572
- ### UserBranchPermissionDialogComponent
573
-
574
- Manages user-to-branch assignments with company filtering.
575
-
576
- ```typescript
577
- import { UserBranchPermissionDialogComponent } from '@flusys/ng-auth';
578
-
579
- @Component({
580
- imports: [UserBranchPermissionDialogComponent],
581
- template: `
582
- <lib-user-branch-permission-dialog
583
- [visible]="showDialog()"
584
- [user]="selectedUser()"
585
- (closed)="showDialog.set(false)"
586
- (permissionsChanged)="reloadData()" />
587
- `
588
- })
589
- ```
590
-
591
- **Features:**
592
- - Company dropdown shows only user's permitted companies
593
- - Branch list loads on company selection
594
- - Immediate save on toggle
595
- - Contextual empty messages (no permissions vs no branches)
596
- - Scoped to user's permitted companies
597
-
598
- **Workflow:**
599
- 1. Dialog opens → Loads user's permitted companies via effect()
600
- 2. User selects company → Loads branches for that company
601
- 3. User toggles branch checkboxes → Immediately assigns/revokes
602
-
603
- ---
604
-
605
- ## Routes
606
-
607
- ### AUTH_ROUTES
608
-
609
- Public authentication routes.
610
-
611
- ```typescript
612
- import { AUTH_ROUTES } from '@flusys/ng-auth';
613
-
614
- // Routes: /auth/login, /auth/register
615
- { path: 'auth', children: AUTH_ROUTES }
616
- ```
617
-
618
- ### ADMINISTRATION_ROUTES
619
-
620
- Protected administration routes.
621
-
622
- ```typescript
623
- import { ADMINISTRATION_ROUTES } from '@flusys/ng-auth';
624
-
625
- // Routes: /administration/users/*, /administration/company/*, /administration/branch/*
626
- {
627
- path: 'administration',
628
- canActivate: [authGuard],
629
- children: ADMINISTRATION_ROUTES
630
- }
631
- ```
632
-
633
- ### PROFILE_ROUTES
634
-
635
- User profile page routes.
636
-
637
- ```typescript
638
- import { PROFILE_ROUTES } from '@flusys/ng-auth';
639
-
640
- // Route: /profile
641
- {
642
- path: 'profile',
643
- canActivate: [authGuard],
644
- children: PROFILE_ROUTES
645
- }
646
- ```
647
-
648
- ---
649
-
650
- ## Setup
651
-
652
- ### app.config.ts
653
-
654
- ```typescript
655
- import { ApplicationConfig } from '@angular/core';
656
- import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
657
- import { APP_CONFIG, errorCatchingInterceptor } from '@flusys/ng-core';
658
- import {
659
- authInterceptor,
660
- tokenRefreshInterceptor,
661
- provideAuthLayoutIntegration,
662
- provideAuthProviders
663
- } from '@flusys/ng-auth';
664
- import { provideIamProviders } from '@flusys/ng-iam';
665
- import { provideStorageProviders } from '@flusys/ng-storage';
20
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
21
+ import { provideAuthProviders, provideAuthLayoutIntegration, authInterceptor, tokenRefreshInterceptor } from '@flusys/ng-auth';
666
22
 
667
23
  export const appConfig: ApplicationConfig = {
668
24
  providers: [
669
25
  provideHttpClient(
670
- withFetch(),
671
26
  withInterceptors([
672
- authInterceptor,
673
- tokenRefreshInterceptor,
674
- errorCatchingInterceptor
27
+ authInterceptor, // attaches Bearer token; sends withCredentials for cookie endpoints
28
+ tokenRefreshInterceptor, // proactive + reactive refresh; queues concurrent 401s
675
29
  ])
676
30
  ),
677
- { provide: APP_CONFIG, useValue: DEFAULT_APP_CONFIG },
678
- ...provideAuthLayoutIntegration(),
679
- ...provideAuthProviders(),
680
- ...provideIamProviders(), // Enables permissions display on profile page
681
- ...provideStorageProviders(), // Enables profile picture upload
682
- ]
31
+ ...provideAuthProviders(), // USER_PROVIDER, COMPANY_API_PROVIDER, USER_PERMISSION_PROVIDER, AUTH_STATE_PROVIDER
32
+ ...provideAuthLayoutIntegration(), // LAYOUT_AUTH_STATE, LAYOUT_AUTH_API
33
+ ],
683
34
  };
684
35
  ```
685
36
 
686
- ### app.routes.ts
37
+ Interceptor order matters: `authInterceptor` must come before `tokenRefreshInterceptor`.
38
+
39
+ ## 2. Add Routes
687
40
 
688
41
  ```typescript
689
- import { Routes } from '@angular/router';
690
- import { authGuard, guestGuard, AUTH_ROUTES, ADMINISTRATION_ROUTES, PROFILE_ROUTES } from '@flusys/ng-auth';
42
+ // app.routes.ts
43
+ import { AUTH_ROUTES, ADMINISTRATION_ROUTES, PROFILE_ROUTES } from '@flusys/ng-auth';
691
44
 
692
45
  export const routes: Routes = [
693
- { path: 'auth', children: AUTH_ROUTES },
694
46
  {
695
- path: '',
696
- canActivate: [authGuard],
697
- children: [
698
- { path: 'dashboard', loadComponent: () => import('./dashboard.component') },
699
- { path: 'administration', children: ADMINISTRATION_ROUTES },
700
- { path: 'profile', children: PROFILE_ROUTES }
701
- ]
702
- }
47
+ path: 'auth',
48
+ loadChildren: () => AUTH_ROUTES, // login, register, forgot-password, reset-password, verify-email
49
+ },
50
+ {
51
+ path: 'administration',
52
+ loadChildren: () => ADMINISTRATION_ROUTES, // users, company, branch (protected by authGuard)
53
+ },
54
+ {
55
+ path: 'profile',
56
+ loadChildren: () => PROFILE_ROUTES, // profile page (protected by authGuard)
57
+ },
703
58
  ];
704
59
  ```
705
60
 
706
- ---
707
-
708
- ## Interfaces
709
-
710
- ### Request DTOs
711
-
712
- ```typescript
713
- interface ILoginRequest { email: string; password: string; }
714
- interface IRegistrationRequest { name: string; email: string; password: string; phone?: string; }
715
- interface ISelectRequest { companyId: string; branchId?: string; sessionId: string; }
716
- interface ISwitchCompanyRequest { companyId: string; branchId: string; }
717
- interface IChangePasswordRequest { currentPassword: string; newPassword: string; }
718
- ```
719
-
720
- ### Response DTOs
61
+ ## 3. Protect Routes with Guards
721
62
 
722
63
  ```typescript
723
- interface ILoginResponse {
724
- accessToken: string;
725
- user: IUserInfo;
726
- requiresSelection?: boolean;
727
- sessionId?: string;
728
- companies?: ICompanyWithBranches[];
729
- expiresAt?: number;
730
- }
731
-
732
- interface ICompanySelectionResponse {
733
- accessToken: string;
734
- user: IUserInfo;
735
- company: ICompanyInfo;
736
- branch: IBranchInfo;
737
- }
738
-
739
- interface IMeResponse extends IUserInfo {}
740
- interface IRefreshTokenResponse { accessToken: string; }
741
- ```
742
-
743
- ### Entity DTOs
744
-
745
- ```typescript
746
- interface IUser {
747
- id: string;
748
- name: string;
749
- email: string;
750
- phone?: string;
751
- profilePictureId?: string;
752
- isActive: boolean;
753
- isEmailVerified: boolean;
754
- isPhoneVerified: boolean;
755
- lastLoginAt?: Date;
756
- createdAt: Date;
757
- updatedAt: Date;
758
- }
759
-
760
- interface ICompany {
761
- id: string;
762
- name: string;
763
- slug: string;
764
- logoId?: string;
765
- address?: string;
766
- phone?: string;
767
- email?: string;
768
- website?: string;
769
- isActive: boolean;
770
- createdAt: Date;
771
- updatedAt: Date;
772
- }
773
-
774
- interface IBranch {
775
- id: string;
776
- name: string;
777
- slug: string;
778
- companyId: string;
779
- parentId?: string;
780
- logoId?: string;
781
- address?: string;
782
- phone?: string;
783
- email?: string;
784
- isActive: boolean;
785
- createdAt: Date;
786
- updatedAt: Date;
787
- }
788
- ```
64
+ import { authGuard, guestGuard, companyFeatureGuard } from '@flusys/ng-auth';
789
65
 
790
- ### Permission DTOs
791
-
792
- ```typescript
793
- interface IPermissionItem { targetId: string; isAdd: boolean; }
794
- interface IAssignUserCompanyRequest { userId: string; items: IPermissionItem[]; }
795
- interface IAssignUserBranchRequest { userId: string; items: IPermissionItem[]; }
796
- interface IBatchOperationResponse { success: boolean; message: string; }
797
- interface IUserCompanyPermission { id: string; userId: string; companyId: string; companyName: string; companySlug: string; }
798
- interface IUserBranchPermission { id: string; userId: string; branchId: string; branchName: string; branchSlug: string; companyId: string; }
66
+ const routes: Routes = [
67
+ {
68
+ path: 'dashboard',
69
+ canActivate: [authGuard], // redirects to /auth/login if not authenticated
70
+ loadComponent: () => import('./dashboard.component'),
71
+ },
72
+ {
73
+ path: 'auth/login',
74
+ canActivate: [guestGuard], // redirects to '/' if already authenticated; restores session first
75
+ loadComponent: () => import('./login.component'),
76
+ },
77
+ ];
799
78
  ```
800
79
 
801
- ---
802
-
803
- ## Best Practices
80
+ | Guard | Exported name | Behavior |
81
+ |---|---|---|
82
+ | Require auth | `authGuard` | Redirects to `/auth/login`; checks company if `enableCompanyFeature` is on |
83
+ | Require guest | `guestGuard` | Redirects to `/` (or `returnUrl`) if authenticated; calls `AuthInitService.initialize()` |
84
+ | Require company feature | `companyFeatureGuard` | Redirects to `/` if company feature is disabled in config |
804
85
 
805
- ### Angular 21 Component Patterns
86
+ ## 4. Read Auth State
806
87
 
807
- All components in ng-auth follow these patterns:
88
+ `AuthStateService` holds all auth state as read-only signals. The access token is memory-only (never persisted).
808
89
 
809
90
  ```typescript
810
- @Component({
811
- selector: 'lib-example',
812
- standalone: true,
813
- changeDetection: ChangeDetectionStrategy.OnPush, // Always OnPush
814
- imports: [AngularModule, PrimeModule, FormField],
815
- template: `...`
816
- })
817
- export class ExampleComponent {
818
- // Dependency injection via inject()
819
- private readonly service = inject(MyService);
820
-
821
- // Signal-based state
822
- readonly isLoading = signal(false);
823
- readonly data = signal<IData | null>(null);
824
-
825
- // Computed signals
826
- readonly isValid = computed(() => this.data() !== null);
91
+ import { AuthStateService } from '@flusys/ng-auth';
827
92
 
828
- // Signal Forms
829
- readonly formModel = signal<IFormModel>({ name: '', email: '' });
830
- readonly formTree = form(this.formModel, (f) => {
831
- required(f.name, { message: 'Name is required' });
832
- });
93
+ @Component({ ... })
94
+ export class MyComponent {
95
+ private readonly authState = inject(AuthStateService);
833
96
 
834
- // Error handling with proper typing
835
- async onSubmit(): Promise<void> {
836
- try {
837
- // ...
838
- } catch (error: unknown) {
839
- const err = error as { error?: { message?: string } };
840
- // Handle error
841
- }
842
- }
97
+ readonly user = this.authState.user; // Signal<IUserInfo | null>
98
+ readonly company = this.authState.company; // Signal<ICompanyInfo | null>
99
+ readonly branch = this.authState.branch; // Signal<IBranchInfo | null>
100
+ readonly isAuth = this.authState.isAuthenticated; // Signal<boolean>
101
+ readonly tokenExpired = this.authState.isTokenExpired; // Signal<boolean>
843
102
  }
844
103
  ```
845
104
 
846
- ### Responsive Design Patterns
847
-
848
- ```html
849
- <!-- Responsive container -->
850
- <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 p-4">
851
-
852
- <!-- Two-column form grid -->
853
- <form class="grid grid-cols-1 md:grid-cols-2 gap-4">
854
-
855
- <!-- Right-aligned buttons -->
856
- <div class="flex justify-end gap-4 md:col-span-2 pt-4">
857
-
858
- <!-- Horizontally scrollable tabs -->
859
- <div class="scrollbar-hide flex gap-1 overflow-x-auto flex-nowrap">
860
-
861
- <!-- Responsive table wrapper -->
862
- <div class="overflow-x-auto">
863
- <p-table [scrollable]="true" [paginator]="total() > 0">
864
- ```
865
-
866
- ### Component Organization
867
-
868
- ```
869
- projects/ng-auth/
870
- ├── components/ ← Shared dialogs
871
- │ ├── user-company-permission-dialog.component.ts
872
- │ └── user-branch-permission-dialog.component.ts
873
- ├── pages/ ← Route-specific pages
874
- │ ├── login/
875
- │ ├── register/
876
- │ ├── forgot-password/
877
- │ ├── reset-password/
878
- │ ├── verify-email/
879
- │ ├── profile/
880
- │ ├── administration/
881
- │ ├── users/
882
- │ ├── company/
883
- │ └── branch/
884
- ├── services/
885
- ├── guards/
886
- ├── interceptors/
887
- └── adapters/
888
- ```
889
-
890
- **Rule:** Dialog components go in `components/`, page components go in `pages/`.
891
-
892
- ### Permission Filtering Pattern
893
-
894
- When building permission UI, filter by user's permitted scope:
105
+ Key state methods:
895
106
 
896
107
  ```typescript
897
- // ✅ CORRECT - Filter by user's permitted companies
898
- const response = await firstValueFrom(permApi.getUserCompanies(userId));
899
- const userCompanies = response.data;
900
-
901
- // ❌ WRONG - Don't show all companies
902
- const allCompanies = await companyApi.fetchList(); // Security risk
108
+ authState.setLoginState(data); // sets user + token + company + branch
109
+ authState.setSelectionState(data); // multi-company selection flow
110
+ authState.resetState(); // logout — clears all state + localStorage
111
+ authState.navigateToLogin(returnUrl?); // safe redirect (rejects external URLs)
903
112
  ```
904
113
 
905
- ### Token Management
906
-
907
- - Use httpOnly cookies for refresh tokens (backend)
908
- - Access tokens stored in memory via AuthStateService
909
- - TokenRefreshInterceptor handles automatic refresh
910
- - Auto-persistence for company/branch selection
114
+ ## 6. Session Restore
911
115
 
912
- ### Type Safety
913
-
914
- Avoid `any` types - use explicit interfaces:
116
+ `AuthInitService.initialize()` runs `POST /auth/refresh` (HTTP-only cookie) then `POST /auth/me` to restore session in memory. It is called automatically by `guestGuard`. Call it manually if needed:
915
117
 
916
118
  ```typescript
917
- // ✅ CORRECT - Explicit response type
918
- .subscribe({
919
- next: (result: { message?: string }) => {
920
- this.messageService.add({
921
- severity: 'success',
922
- detail: result.message,
923
- });
924
- },
925
- });
119
+ import { AuthInitService } from '@flusys/ng-auth';
120
+
121
+ const authInit = inject(AuthInitService);
926
122
 
927
- // ❌ WRONG - Using any
928
- .subscribe({
929
- next: (result: any) => { ... },
123
+ authInit.initialize().subscribe(isAuthenticated => {
124
+ // safe to read authState signals now
930
125
  });
931
126
  ```
932
127
 
933
- ### Multi-Company Flow
128
+ The `initialized` signal is `true` after the first call completes (success or failure).
934
129
 
935
- 1. Login → Check if `requiresSelection: true`
936
- 2. If yes → Show Step 2 (company/branch selection)
937
- 3. Auto-select if only one company/branch
938
- 4. Save selection → Navigate to dashboard
130
+ ## 7. Extend Registration / Profile (Optional)
939
131
 
940
- ---
941
-
942
- ## Common Issues
943
-
944
- ### Guards Not Working
132
+ Inject custom fields and sections via extension tokens:
945
133
 
946
- **Problem:** Routes accessible without authentication.
947
-
948
- **Solution:** Ensure authGuard is on parent route and session is restored:
949
134
  ```typescript
950
- {
951
- path: '',
952
- canActivate: [authGuard], // Parent guard
953
- children: [...]
954
- }
955
- ```
956
-
957
- ### Token Not Sent
958
-
959
- **Problem:** 401 on all requests.
960
-
961
- **Solution:** Ensure interceptor order is correct:
962
- ```typescript
963
- withInterceptors([
964
- authInterceptor, // First: adds token
965
- tokenRefreshInterceptor, // Second: handles 401
966
- errorCatchingInterceptor // Last: shows errors
967
- ])
968
- ```
135
+ import { AUTH_REGISTRATION_EXTENSION, AUTH_PROFILE_EXTENSION, AUTH_USER_LIST_EXTENSION, AUTH_VALIDATION_CONFIG } from '@flusys/ng-auth';
969
136
 
970
- ### Layout Not Showing User
971
-
972
- **Problem:** User info missing from topbar.
973
-
974
- **Solution:** Ensure layout integration is provided:
975
- ```typescript
976
137
  providers: [
977
- ...provideAuthLayoutIntegration()
978
- ]
979
- ```
980
-
981
- ### Company Switcher Shows All Companies
982
-
983
- **Problem:** Switcher shows companies user doesn't have access to.
984
-
985
- **Solution:** `AuthLayoutApiAdapter.getUserCompanies()` uses `UserPermissionApiService.getUserCompanies(userId)` internally - ensure user has company permissions assigned.
986
-
987
- ---
988
-
989
- ## No-IAM Mode (Bypassing Permission Guards)
990
-
991
- When you don't use the `@flusys/ng-iam` module but your routes use `permissionGuard`, the guard will fail because no permissions are loaded.
992
-
993
- **Solution:** Set wildcard permissions in `PermissionValidatorService` after login.
994
-
995
- ### How Permission Guard Works
996
-
997
- ```
998
- Route with permissionGuard('user.view')
999
- ↓
1000
- permissionGuard() → inject(PermissionValidatorService)
1001
- ↓
1002
- permissionValidator.isPermissionsLoaded() → false
1003
- ↓
1004
- Redirect to '/' ❌
1005
- ```
1006
-
1007
- ### Fix: Set Wildcard Permissions After Login
1008
-
1009
- #### Option 1: In Login Flow (Recommended)
1010
-
1011
- Update your login handling to set permissions after successful authentication:
1012
-
1013
- ```typescript
1014
- // login.component.ts or auth.service.ts
1015
- import { PermissionValidatorService } from '@flusys/ng-shared';
1016
-
1017
- @Component({ ... })
1018
- export class LoginComponent {
1019
- private readonly permissionValidator = inject(PermissionValidatorService);
1020
- private readonly authApi = inject(AuthApiService);
1021
- private readonly authState = inject(AuthStateService);
1022
-
1023
- async onLogin(): Promise<void> {
1024
- const response = await firstValueFrom(this.authApi.login(credentials));
1025
- this.authState.setLoginState(response);
1026
-
1027
- // When IAM is NOT used, grant all permissions via wildcard
1028
- this.permissionValidator.setPermissions(['*']);
1029
-
1030
- // Navigate to dashboard
1031
- this.router.navigate(['/dashboard']);
1032
- }
1033
- }
1034
- ```
1035
-
1036
- #### Option 2: In APP_INITIALIZER
1037
-
1038
- Set permissions at app startup:
1039
-
1040
- ```typescript
1041
- // app.config.ts
1042
- import { APP_INITIALIZER } from '@angular/core';
1043
- import { PermissionValidatorService } from '@flusys/ng-shared';
1044
-
1045
- export const appConfig: ApplicationConfig = {
1046
- providers: [
1047
- {
1048
- provide: APP_INITIALIZER,
1049
- useFactory: (permissionValidator: PermissionValidatorService) => {
1050
- return () => {
1051
- // Set wildcard permissions when IAM is disabled
1052
- permissionValidator.setPermissions(['*']);
1053
- };
1054
- },
1055
- deps: [PermissionValidatorService],
1056
- multi: true,
138
+ { provide: AUTH_REGISTRATION_EXTENSION, useClass: MyRegistrationExtension }, // extra fields on register form
139
+ { provide: AUTH_PROFILE_EXTENSION, useClass: MyProfileExtension }, // extra sections on profile page
140
+ { provide: AUTH_USER_LIST_EXTENSION, useClass: MyUserListExtension }, // extra columns/actions on user list
141
+ {
142
+ provide: AUTH_VALIDATION_CONFIG,
143
+ useValue: {
144
+ password: { minLength: 10, requireUppercase: true, requireSpecialChars: true },
1057
145
  },
1058
- // ... other providers
1059
- ],
1060
- };
1061
- ```
1062
-
1063
- #### Option 3: In AuthInitService (After Session Restore)
1064
-
1065
- If you restore sessions on page refresh, set permissions there:
1066
-
1067
- ```typescript
1068
- // After successful /auth/me response
1069
- authInit.initialize().subscribe({
1070
- next: (user) => {
1071
- // Session restored, set wildcard permissions
1072
- permissionValidator.setPermissions(['*']);
1073
146
  },
1074
- });
1075
- ```
1076
-
1077
- ### Wildcard Permission Syntax
1078
-
1079
- The `permissionGuard` utilities support these patterns:
1080
-
1081
- | Pattern | Meaning |
1082
- |---------|---------|
1083
- | `*` | Full access to everything |
1084
- | `user.*` | All user permissions |
1085
- | `branch.*` | All branch permissions |
1086
-
1087
- ### For Jest Tests
1088
-
1089
- Mock `PermissionValidatorService` in your test setup:
1090
-
1091
- ```typescript
1092
- import { PermissionValidatorService } from '@flusys/ng-shared';
1093
-
1094
- describe('MyComponent', () => {
1095
- beforeEach(async () => {
1096
- await TestBed.configureTestingModule({
1097
- imports: [MyComponent],
1098
- providers: [
1099
- {
1100
- provide: PermissionValidatorService,
1101
- useValue: {
1102
- permissions: () => ['*'],
1103
- isPermissionsLoaded: () => true,
1104
- hasPermission: () => true,
1105
- setPermissions: jest.fn(),
1106
- clearPermissions: jest.fn(),
1107
- },
1108
- },
1109
- ],
1110
- }).compileComponents();
1111
- });
1112
- });
1113
- ```
1114
-
1115
- ### For Template Permission Checks
1116
-
1117
- If using `*hasPermission` directive, it will also respect wildcard:
1118
-
1119
- ```html
1120
- <!-- This will show when permissions include '*' -->
1121
- <button *hasPermission="'user.delete'">Delete User</button>
147
+ ]
1122
148
  ```
1123
149
 
1124
- ---
1125
-
1126
- ## See Also
1127
-
1128
- - [CORE-GUIDE.md](./CORE-GUIDE.md) - APP_CONFIG configuration
1129
- - [SHARED-GUIDE.md](./SHARED-GUIDE.md) - Provider interfaces
1130
- - [LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md) - Layout integration
1131
- - [IAM-GUIDE.md](./IAM-GUIDE.md) - Permission management
1132
- - [../FLUSYS_NEST/docs/AUTH-GUIDE.md](../../FLUSYS_NEST/docs/AUTH-GUIDE.md) - Backend auth module
1133
-
1134
- ---
1135
-
1136
- **Last Updated:** 2026-02-23
1137
- **Version:** 1.1.0
1138
- **Angular Version:** 21
1139
-
1140
- ## Component Checklist
150
+ ## License
1141
151
 
1142
- | Component | OnPush | Signal Forms | Responsive | Dark Mode |
1143
- |-----------|--------|--------------|------------|-----------|
1144
- | LoginPageComponent | ✅ | ✅ | ✅ | ✅ |
1145
- | RegisterPageComponent | ✅ | ✅ | ✅ | ✅ |
1146
- | ForgotPasswordPageComponent | ✅ | ✅ | ✅ | ✅ |
1147
- | ResetPasswordPageComponent | ✅ | ✅ | ✅ | ✅ |
1148
- | VerifyEmailPageComponent | ✅ | ✅ | ✅ | ✅ |
1149
- | ProfilePageComponent | ✅ | ✅ | ✅ | ✅ |
1150
- | AdministrationPageComponent | ✅ | N/A | ✅ | ✅ |
1151
- | UserListComponent | ✅ | N/A | ✅ | ✅ |
1152
- | UserFormComponent | ✅ | ✅ | ✅ | ✅ |
1153
- | CompanyListComponent | ✅ | N/A | ✅ | ✅ |
1154
- | CompanyFormComponent | ✅ | ✅ | ✅ | ✅ |
1155
- | BranchListComponent | ✅ | N/A | ✅ | ✅ |
1156
- | BranchFormComponent | ✅ | ✅ | ✅ | ✅ |
1157
- | UserCompanyPermissionDialogComponent | ✅ | N/A | ✅ | ✅ |
1158
- | UserBranchPermissionDialogComponent | ✅ | N/A | ✅ | ✅ |
152
+ MIT © FLUSYS