@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 +87 -1093
- package/fesm2022/flusys-ng-auth.mjs +5649 -2955
- package/fesm2022/flusys-ng-auth.mjs.map +1 -1
- package/package.json +11 -11
- package/types/flusys-ng-auth.d.ts +394 -284
package/README.md
CHANGED
|
@@ -1,1158 +1,152 @@
|
|
|
1
|
-
# @flusys/ng-auth
|
|
1
|
+
# @flusys/ng-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
JWT authentication library for FLUSYS Angular apps — login, session restore, token refresh, multi-company selection, and provider adapter registration.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@flusys/ng-auth)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
352
7
|
|
|
353
8
|
---
|
|
354
9
|
|
|
355
|
-
##
|
|
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
|
-
```
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
678
|
-
...provideAuthLayoutIntegration(),
|
|
679
|
-
|
|
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
|
-
|
|
37
|
+
Interceptor order matters: `authInterceptor` must come before `tokenRefreshInterceptor`.
|
|
38
|
+
|
|
39
|
+
## 2. Add Routes
|
|
687
40
|
|
|
688
41
|
```typescript
|
|
689
|
-
|
|
690
|
-
import {
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
## 4. Read Auth State
|
|
806
87
|
|
|
807
|
-
|
|
88
|
+
`AuthStateService` holds all auth state as read-only signals. The access token is memory-only (never persisted).
|
|
808
89
|
|
|
809
90
|
```typescript
|
|
810
|
-
@
|
|
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
|
-
|
|
829
|
-
|
|
830
|
-
readonly
|
|
831
|
-
required(f.name, { message: 'Name is required' });
|
|
832
|
-
});
|
|
93
|
+
@Component({ ... })
|
|
94
|
+
export class MyComponent {
|
|
95
|
+
private readonly authState = inject(AuthStateService);
|
|
833
96
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
//
|
|
838
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
next: (result: any) => { ... },
|
|
123
|
+
authInit.initialize().subscribe(isAuthenticated => {
|
|
124
|
+
// safe to read authState signals now
|
|
930
125
|
});
|
|
931
126
|
```
|
|
932
127
|
|
|
933
|
-
|
|
128
|
+
The `initialized` signal is `true` after the first call completes (success or failure).
|
|
934
129
|
|
|
935
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
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
|