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