@flusys/ng-auth 4.1.1 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,67 +1,9 @@
1
1
  # @flusys/ng-auth
2
2
 
3
- > Complete authentication library for the FLUSYS Angular platform — JWT session management, HTTP-only cookie tokens, multi-company selection, and provider adapter registration.
3
+ JWT authentication library for FLUSYS Angular apps — login, session restore, token refresh, multi-company selection, and provider adapter registration.
4
4
 
5
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)
9
-
10
- ---
11
-
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)
31
-
32
- ---
33
-
34
- ## Overview
35
-
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.
37
-
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.
39
-
40
- ---
41
-
42
- ## Features
43
-
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`
55
-
56
- ---
57
-
58
- ## Compatibility
59
-
60
- | Package | Version |
61
- |---------|---------|
62
- | Angular | 21+ |
63
- | @flusys/ng-core | 4.x |
64
- | @flusys/ng-shared | 4.x |
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
65
7
 
66
8
  ---
67
9
 
@@ -71,406 +13,140 @@ Tokens are stored in **HTTP-only cookies** (managed by the backend) — never in
71
13
  npm install @flusys/ng-auth @flusys/ng-core @flusys/ng-shared
72
14
  ```
73
15
 
74
- ---
75
-
76
- ## Quick Start
77
-
78
- ### 1. Register Auth Providers
16
+ ## 1. Register Providers
79
17
 
80
18
  ```typescript
81
19
  // app.config.ts
82
- import { ApplicationConfig } from '@angular/core';
83
20
  import { provideHttpClient, withInterceptors } from '@angular/common/http';
84
- import { APP_CONFIG } from '@flusys/ng-core';
85
- import {
86
- provideAuthProviders,
87
- provideAuthLayoutIntegration,
88
- authInterceptor,
89
- tokenRefreshInterceptor,
90
- } from '@flusys/ng-auth';
91
- import { environment } from './environments/environment';
21
+ import { provideAuthProviders, provideAuthLayoutIntegration, authInterceptor, tokenRefreshInterceptor } from '@flusys/ng-auth';
92
22
 
93
23
  export const appConfig: ApplicationConfig = {
94
24
  providers: [
95
- { provide: APP_CONFIG, useValue: environment },
96
25
  provideHttpClient(
97
26
  withInterceptors([
98
- authInterceptor, // Attaches access token to requests
99
- tokenRefreshInterceptor, // Handles 401 → refresh → retry
27
+ authInterceptor, // attaches Bearer token; sends withCredentials for cookie endpoints
28
+ tokenRefreshInterceptor, // proactive + reactive refresh; queues concurrent 401s
100
29
  ])
101
30
  ),
102
- ...provideAuthProviders(), // USER_PROVIDER, COMPANY_PROVIDER
103
- ...provideAuthLayoutIntegration(), // LAYOUT_AUTH_STATE, LAYOUT_AUTH_API
31
+ ...provideAuthProviders(), // USER_PROVIDER, COMPANY_API_PROVIDER, USER_PERMISSION_PROVIDER, AUTH_STATE_PROVIDER
32
+ ...provideAuthLayoutIntegration(), // LAYOUT_AUTH_STATE, LAYOUT_AUTH_API
104
33
  ],
105
34
  };
106
35
  ```
107
36
 
108
- ### 2. Protect Routes with Guards
37
+ Interceptor order matters: `authInterceptor` must come before `tokenRefreshInterceptor`.
38
+
39
+ ## 2. Add Routes
109
40
 
110
41
  ```typescript
111
42
  // app.routes.ts
112
- import { Routes } from '@angular/router';
113
- import { appInitGuard } from '@flusys/ng-auth';
114
- import { AppLayoutComponent } from '@flusys/ng-layout';
43
+ import { AUTH_ROUTES, ADMINISTRATION_ROUTES, PROFILE_ROUTES } from '@flusys/ng-auth';
115
44
 
116
45
  export const routes: Routes = [
117
46
  {
118
- path: '',
119
- component: AppLayoutComponent,
120
- canActivate: [appInitGuard], // Restores session on page refresh
121
- children: [
122
- { path: 'dashboard', loadComponent: () => import('./pages/dashboard.component') },
123
- ],
47
+ path: 'auth',
48
+ loadChildren: () => AUTH_ROUTES, // login, register, forgot-password, reset-password, verify-email
124
49
  },
125
50
  {
126
- path: 'auth',
127
- loadChildren: () => import('@flusys/ng-auth').then(m => m.AUTH_ROUTES),
51
+ path: 'administration',
52
+ loadChildren: () => ADMINISTRATION_ROUTES, // users, company, branch (protected by authGuard)
128
53
  },
129
- ];
130
- ```
131
-
132
- ### 3. Enable Auth in App Config
133
-
134
- ```typescript
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
- },
54
+ {
55
+ path: 'profile',
56
+ loadChildren: () => PROFILE_ROUTES, // profile page (protected by authGuard)
147
57
  },
148
- };
149
- ```
150
-
151
- ---
152
-
153
- ## Module Registration
154
-
155
- `ng-auth` uses function-based providers (not Angular modules):
156
-
157
- ### `provideAuthProviders()`
158
-
159
- Registers auth adapters against shared provider tokens:
160
-
161
- ```typescript
162
- ...provideAuthProviders()
163
-
164
- // Provides:
165
- // USER_PROVIDER → AuthStateService (IUserProvider)
166
- // COMPANY_PROVIDER → AuthStateService (ICompanyProvider)
58
+ ];
167
59
  ```
168
60
 
169
- These tokens are consumed by `ng-iam`, `ng-storage`, `ng-notification` — no direct dependency on `ng-auth`.
170
-
171
- ### `provideAuthLayoutIntegration()`
172
-
173
- Bridges auth state and API to the layout:
61
+ ## 3. Protect Routes with Guards
174
62
 
175
63
  ```typescript
176
- ...provideAuthLayoutIntegration()
177
-
178
- // Provides:
179
- // LAYOUT_AUTH_STATE → AuthLayoutStateAdapter (profile picture, company logo)
180
- // LAYOUT_AUTH_API → AuthLayoutApiAdapter (logout, select-company actions)
181
- ```
182
-
183
- ---
184
-
185
- ## Session Restoration Flow
64
+ import { authGuard, guestGuard, companyFeatureGuard } from '@flusys/ng-auth';
186
65
 
187
- On every page refresh, auth state is cleared from memory. The `appInitGuard` restores the session transparently:
188
-
189
- ```
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
206
- ```
207
-
208
- ### Company Selection Flow
209
-
210
- ```
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
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
+ ];
225
78
  ```
226
79
 
227
- ---
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 |
228
85
 
229
- ## Auth State Service
86
+ ## 4. Read Auth State
230
87
 
231
- `AuthStateService` is the single source of truth for authentication state, powered by Angular signals.
88
+ `AuthStateService` holds all auth state as read-only signals. The access token is memory-only (never persisted).
232
89
 
233
90
  ```typescript
234
91
  import { AuthStateService } from '@flusys/ng-auth';
235
92
 
236
93
  @Component({ ... })
237
94
  export class MyComponent {
238
- private authState = inject(AuthStateService);
239
-
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>
246
-
247
- // Computed
248
- userName = computed(() => this.authState.user()?.name ?? 'Guest');
249
- }
250
- ```
251
-
252
- **AuthStateService API:**
253
-
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) |
95
+ private readonly authState = inject(AuthStateService);
265
96
 
266
- ---
267
-
268
- ## Auth API Service
269
-
270
- `AuthApiService` handles all HTTP calls to the auth backend.
271
-
272
- ```typescript
273
- import { AuthApiService } from '@flusys/ng-auth';
274
-
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
- }
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>
287
102
  }
288
103
  ```
289
104
 
290
- **AuthApiService Methods:**
291
-
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 |
304
-
305
- ---
306
-
307
- ## HTTP Interceptors
308
-
309
- ### authInterceptor
310
-
311
- Attaches the in-memory access token as `Authorization: Bearer <token>` header to every outgoing request (except public endpoints).
105
+ Key state methods:
312
106
 
313
107
  ```typescript
314
- provideHttpClient(
315
- withInterceptors([authInterceptor])
316
- )
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)
317
112
  ```
318
113
 
319
- ### tokenRefreshInterceptor
320
-
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
114
+ ## 6. Session Restore
325
115
 
326
- All concurrent 401s are queued — only one refresh call is made.
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:
327
117
 
328
118
  ```typescript
329
- provideHttpClient(
330
- withInterceptors([tokenRefreshInterceptor])
331
- )
332
- ```
333
-
334
- **Interceptor Order Matters:**
119
+ import { AuthInitService } from '@flusys/ng-auth';
335
120
 
336
- ```typescript
337
- withInterceptors([
338
- authInterceptor, // Must be FIRST (attaches token)
339
- tokenRefreshInterceptor, // Must be SECOND (handles 401)
340
- ])
341
- ```
342
-
343
- ---
344
-
345
- ## Guards
346
-
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 |
353
-
354
- ```typescript
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
- ],
366
- }
367
- ```
368
-
369
- ---
370
-
371
- ## Pages
372
-
373
- `ng-auth` exports pre-built page components routable via `AUTH_ROUTES`:
374
-
375
- ```typescript
376
- import { AUTH_ROUTES } from '@flusys/ng-auth';
121
+ const authInit = inject(AuthInitService);
377
122
 
378
- // app.routes.ts
379
- { path: 'auth', loadChildren: () => AUTH_ROUTES }
123
+ authInit.initialize().subscribe(isAuthenticated => {
124
+ // safe to read authState signals now
125
+ });
380
126
  ```
381
127
 
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 |
389
-
390
- All pages support full i18n via `TranslatePipe`.
391
-
392
- ---
393
-
394
- ## Provider Adapters
128
+ The `initialized` signal is `true` after the first call completes (success or failure).
395
129
 
396
- ### AUTH_EMAIL_PROVIDER
130
+ ## 7. Extend Registration / Profile (Optional)
397
131
 
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.
399
-
400
- ### User Enricher
401
-
402
- You can inject additional user data at login time:
132
+ Inject custom fields and sections via extension tokens:
403
133
 
404
134
  ```typescript
405
- // Custom enricher (rare — usually not needed)
406
- import { USER_ENRICHER } from '@flusys/ng-auth';
135
+ import { AUTH_REGISTRATION_EXTENSION, AUTH_PROFILE_EXTENSION, AUTH_USER_LIST_EXTENSION, AUTH_VALIDATION_CONFIG } from '@flusys/ng-auth';
407
136
 
408
137
  providers: [
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
409
141
  {
410
- provide: USER_ENRICHER,
411
- useClass: MyUserEnricher,
142
+ provide: AUTH_VALIDATION_CONFIG,
143
+ useValue: {
144
+ password: { minLength: 10, requireUppercase: true, requireSpecialChars: true },
145
+ },
412
146
  },
413
147
  ]
414
148
  ```
415
149
 
416
- ---
417
-
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 |
432
-
433
- ---
434
-
435
- ## Configuration Reference
436
-
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 |
443
-
444
- ---
445
-
446
- ## Troubleshooting
447
-
448
- **Token refresh loop (endless 401 cycle)**
449
-
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:
451
-
452
- ```typescript
453
- // The interceptor automatically skips /auth/refresh to prevent loops
454
- ```
455
-
456
- **`appInitGuard` redirects to login on every refresh**
457
-
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`.
459
-
460
- **Company selection page shown even after selecting**
461
-
462
- Verify that `POST /auth/select` returns both tokens AND the company/branch data. `AuthStateService.setCompany()` must be called with the response.
463
-
464
- **User profile picture doesn't load**
465
-
466
- Profile pictures are fetched via `FileUrlService`, not by constructing URLs. Ensure `ng-storage` providers are registered and `services.storage.enabled: true`.
467
-
468
- **Login form validation errors not translated**
469
-
470
- Provide `TRANSLATE_ADAPTER` via `provideLocalization()`. The fallback text (non-translated) is always shown if the adapter is absent.
471
-
472
- ---
473
-
474
150
  ## License
475
151
 
476
152
  MIT © FLUSYS