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