@flusys/ng-auth 0.1.0-beta.1 → 0.1.0-beta.3
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 +775 -0
- package/package.json +4 -4
package/README.md
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
# @flusys/ng-auth Package Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`@flusys/ng-auth` provides complete authentication functionality including login, registration, token management, company/branch selection, user administration, and permission handling.
|
|
6
|
+
|
|
7
|
+
**Package:** `@flusys/ng-auth`
|
|
8
|
+
**Dependencies:** ng-core, ng-shared, ng-layout
|
|
9
|
+
**Build:** `npm run build:ng-auth`
|
|
10
|
+
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
15
|
+
│ ng-auth │
|
|
16
|
+
├─────────────────────────────────────────────────────────────┤
|
|
17
|
+
│ Services │
|
|
18
|
+
│ ├── AuthApiService (login, register, refresh, logout) │
|
|
19
|
+
│ ├── AuthStateService (signal-based state management) │
|
|
20
|
+
│ ├── AuthInitService (session restoration on refresh) │
|
|
21
|
+
│ ├── TokenRefreshStateService (concurrent refresh handling) │
|
|
22
|
+
│ ├── UserApiService (user CRUD) │
|
|
23
|
+
│ ├── CompanyApiService (company CRUD) │
|
|
24
|
+
│ ├── BranchApiService (branch CRUD) │
|
|
25
|
+
│ └── UserPermissionApiService (permission assignments) │
|
|
26
|
+
├─────────────────────────────────────────────────────────────┤
|
|
27
|
+
│ Guards │
|
|
28
|
+
│ ├── authGuard (requires authentication) │
|
|
29
|
+
│ ├── guestGuard (allows only unauthenticated) │
|
|
30
|
+
│ └── companyFeatureGuard (requires company feature enabled) │
|
|
31
|
+
├─────────────────────────────────────────────────────────────┤
|
|
32
|
+
│ Interceptors │
|
|
33
|
+
│ ├── authInterceptor (adds JWT + tenant headers) │
|
|
34
|
+
│ └── tokenRefreshInterceptor (handles 401 + token refresh) │
|
|
35
|
+
├─────────────────────────────────────────────────────────────┤
|
|
36
|
+
│ Adapters (Provider Interface Pattern) │
|
|
37
|
+
│ ├── AuthLayoutStateAdapter → LAYOUT_AUTH_STATE │
|
|
38
|
+
│ ├── AuthLayoutApiAdapter → LAYOUT_AUTH_API │
|
|
39
|
+
│ ├── AuthUserProviderAdapter → USER_PROVIDER │
|
|
40
|
+
│ ├── AuthCompanyApiProviderAdapter → COMPANY_API_PROVIDER │
|
|
41
|
+
│ └── AuthUserPermissionProviderAdapter → USER_PERMISSION_PROVIDER │
|
|
42
|
+
├─────────────────────────────────────────────────────────────┤
|
|
43
|
+
│ Constants │
|
|
44
|
+
│ └── AUTH_ENDPOINTS (endpoint path definitions) │
|
|
45
|
+
└─────────────────────────────────────────────────────────────┘
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Services
|
|
51
|
+
|
|
52
|
+
### AuthApiService
|
|
53
|
+
|
|
54
|
+
HTTP client for authentication endpoints.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { AuthApiService } from '@flusys/ng-auth';
|
|
58
|
+
|
|
59
|
+
const authApi = inject(AuthApiService);
|
|
60
|
+
|
|
61
|
+
// Authentication
|
|
62
|
+
authApi.login({ email, password }); // POST /auth/login
|
|
63
|
+
authApi.register({ name, email, password }); // POST /auth/register
|
|
64
|
+
authApi.refresh(); // POST /auth/refresh
|
|
65
|
+
authApi.logout(); // POST /auth/logout
|
|
66
|
+
authApi.me(); // GET /auth/me
|
|
67
|
+
|
|
68
|
+
// Company/Branch selection
|
|
69
|
+
authApi.select({ companyId, branchId, sessionId }); // POST /auth/select
|
|
70
|
+
authApi.switchCompany({ companyId, branchId }); // POST /auth/switch-company
|
|
71
|
+
|
|
72
|
+
// Password management
|
|
73
|
+
authApi.changePassword({ currentPassword, newPassword });
|
|
74
|
+
authApi.forgotPassword({ email });
|
|
75
|
+
authApi.resetPassword({ token, newPassword });
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### AuthStateService
|
|
79
|
+
|
|
80
|
+
Signal-based centralized auth state management with auto-persistence.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { AuthStateService } from '@flusys/ng-auth';
|
|
84
|
+
|
|
85
|
+
const authState = inject(AuthStateService);
|
|
86
|
+
|
|
87
|
+
// Read-only signals
|
|
88
|
+
authState.user(); // Signal<IUserInfo | null>
|
|
89
|
+
authState.accessToken(); // Signal<string | null>
|
|
90
|
+
authState.company(); // Signal<ICompanyInfo | null>
|
|
91
|
+
authState.branch(); // Signal<IBranchInfo | null>
|
|
92
|
+
authState.availableCompanies(); // Signal<ICompanyWithBranches[]>
|
|
93
|
+
authState.selectionSessionId(); // Signal<string | null>
|
|
94
|
+
|
|
95
|
+
// Computed signals
|
|
96
|
+
authState.isAuthenticated(); // Signal<boolean>
|
|
97
|
+
authState.requiresCompanySelection(); // Signal<boolean>
|
|
98
|
+
authState.userName(); // Signal<string>
|
|
99
|
+
authState.companyName(); // Signal<string>
|
|
100
|
+
authState.branchName(); // Signal<string>
|
|
101
|
+
authState.tenantId(); // Signal<string | null>
|
|
102
|
+
|
|
103
|
+
// State mutations
|
|
104
|
+
authState.setLoginState(response); // After successful login
|
|
105
|
+
authState.setSelectionState(response); // After company/branch selection
|
|
106
|
+
authState.clearSelectionState(); // Clear pending selection
|
|
107
|
+
authState.resetState(); // Full logout
|
|
108
|
+
authState.navigateToLogin(returnUrl?);
|
|
109
|
+
authState.navigateAfterLogin();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Auto-persistence:** Tokens and company/branch IDs are automatically saved to localStorage via effects.
|
|
113
|
+
|
|
114
|
+
### AuthInitService
|
|
115
|
+
|
|
116
|
+
Handles session restoration on page refresh.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { AuthInitService } from '@flusys/ng-auth';
|
|
120
|
+
|
|
121
|
+
const authInit = inject(AuthInitService);
|
|
122
|
+
|
|
123
|
+
// Signals
|
|
124
|
+
authInit.initialized(); // Signal<boolean>
|
|
125
|
+
authInit.loading(); // Signal<boolean>
|
|
126
|
+
authInit.error(); // Signal<string | null>
|
|
127
|
+
|
|
128
|
+
// Restore session (called by appInitGuard)
|
|
129
|
+
authInit.initialize().subscribe({
|
|
130
|
+
next: (user) => console.log('Session restored:', user),
|
|
131
|
+
error: (err) => console.log('No session or expired')
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Flow:**
|
|
136
|
+
1. Attempts `POST /auth/refresh` with httpOnly cookie
|
|
137
|
+
2. On success, calls `GET /auth/me` to get user info
|
|
138
|
+
3. Updates AuthStateService with restored session
|
|
139
|
+
4. Caches result to prevent duplicate calls
|
|
140
|
+
|
|
141
|
+
### Resource API Services
|
|
142
|
+
|
|
143
|
+
All extend `ApiResourceService` from ng-shared with signal-based state.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { UserApiService, CompanyApiService, BranchApiService } from '@flusys/ng-auth';
|
|
147
|
+
|
|
148
|
+
// Common signals (from ApiResourceService)
|
|
149
|
+
service.data(); // Signal<T[]>
|
|
150
|
+
service.isLoading(); // Signal<boolean>
|
|
151
|
+
service.error(); // Signal<string | null>
|
|
152
|
+
|
|
153
|
+
// Common methods
|
|
154
|
+
await service.insertAsync(data); // Create
|
|
155
|
+
await service.updateAsync(data); // Update
|
|
156
|
+
await service.deleteAsync(id); // Delete
|
|
157
|
+
await service.fetchByIdAsync(id); // Get one
|
|
158
|
+
service.fetchList(search, options); // Get list with pagination
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**UserApiService** - Endpoint: `administration/users`
|
|
162
|
+
```typescript
|
|
163
|
+
userApi.verifyEmail(userId);
|
|
164
|
+
userApi.verifyPhone(userId);
|
|
165
|
+
userApi.updateStatus(userId, status);
|
|
166
|
+
userApi.updateProfile(data);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**CompanyApiService** - Endpoint: `administration/company`
|
|
170
|
+
|
|
171
|
+
**BranchApiService** - Endpoint: `administration/branch`
|
|
172
|
+
```typescript
|
|
173
|
+
branchApi.currentCompanyId(); // Signal for company context
|
|
174
|
+
branchApi.companyBranches(); // Computed: branches filtered by company
|
|
175
|
+
branchApi.setCurrentCompanyId(id);
|
|
176
|
+
branchApi.insertBranch(data); // Auto-sets companyId
|
|
177
|
+
branchApi.fetchByCompany(companyId);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### UserPermissionApiService
|
|
181
|
+
|
|
182
|
+
Manages user-to-company and user-to-branch permission assignments.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { UserPermissionApiService } from '@flusys/ng-auth';
|
|
186
|
+
|
|
187
|
+
const permApi = inject(UserPermissionApiService);
|
|
188
|
+
|
|
189
|
+
// Company permissions
|
|
190
|
+
permApi.assignUserCompanies({
|
|
191
|
+
userId: 'user-1',
|
|
192
|
+
items: [
|
|
193
|
+
{ targetId: 'company-1', isAdd: true }, // Assign
|
|
194
|
+
{ targetId: 'company-2', isAdd: false } // Revoke
|
|
195
|
+
]
|
|
196
|
+
});
|
|
197
|
+
permApi.getUserCompanies(userId); // Get user's companies
|
|
198
|
+
permApi.assignUserToCompany(userId, companyId); // Single assign
|
|
199
|
+
permApi.revokeUserFromCompany(userId, companyId); // Single revoke
|
|
200
|
+
|
|
201
|
+
// Branch permissions
|
|
202
|
+
permApi.assignUserBranches({
|
|
203
|
+
userId: 'user-1',
|
|
204
|
+
items: [
|
|
205
|
+
{ targetId: 'branch-1', isAdd: true }
|
|
206
|
+
]
|
|
207
|
+
});
|
|
208
|
+
permApi.getUserBranches(userId); // Get user's branches
|
|
209
|
+
permApi.assignUserToBranch(userId, branchId);
|
|
210
|
+
permApi.revokeUserFromBranch(userId, branchId);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Backend Endpoints:**
|
|
214
|
+
- `POST /administration/permissions/user-company/assign`
|
|
215
|
+
- `GET /administration/permissions/user-company?userId=xxx`
|
|
216
|
+
- `POST /administration/permissions/user-branch/assign`
|
|
217
|
+
- `GET /administration/permissions/user-branch?userId=xxx`
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Guards
|
|
222
|
+
|
|
223
|
+
### authGuard
|
|
224
|
+
|
|
225
|
+
Protects routes requiring authentication.
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { authGuard } from '@flusys/ng-auth';
|
|
229
|
+
|
|
230
|
+
{
|
|
231
|
+
path: 'dashboard',
|
|
232
|
+
canActivate: [authGuard],
|
|
233
|
+
loadComponent: () => import('./dashboard.component')
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Checks:**
|
|
238
|
+
1. User is authenticated
|
|
239
|
+
2. If company feature enabled: company AND branch are selected
|
|
240
|
+
3. Redirects to `/auth/login` with `returnUrl` if checks fail
|
|
241
|
+
|
|
242
|
+
### guestGuard
|
|
243
|
+
|
|
244
|
+
Protects auth routes (login, register) from authenticated users.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { guestGuard } from '@flusys/ng-auth';
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
path: 'auth/login',
|
|
251
|
+
canActivate: [guestGuard],
|
|
252
|
+
loadComponent: () => import('./login.component')
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Behavior:**
|
|
257
|
+
1. Restores session via `AuthInitService.initialize()` first
|
|
258
|
+
2. If authenticated AND company selected: redirects to home or returnUrl
|
|
259
|
+
3. If selection pending: allows access (login handles Step 2)
|
|
260
|
+
4. If not authenticated: allows access
|
|
261
|
+
|
|
262
|
+
### companyFeatureGuard
|
|
263
|
+
|
|
264
|
+
Enables/disables company management routes based on config.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { companyFeatureGuard } from '@flusys/ng-auth';
|
|
268
|
+
|
|
269
|
+
{
|
|
270
|
+
path: 'administration/company',
|
|
271
|
+
canActivate: [authGuard, companyFeatureGuard],
|
|
272
|
+
loadComponent: () => import('./company-list.component')
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Checks:** `services.auth.enabled` in APP_CONFIG. Redirects to `/` if disabled.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Interceptors
|
|
281
|
+
|
|
282
|
+
### authInterceptor
|
|
283
|
+
|
|
284
|
+
Adds authentication headers to HTTP requests.
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { authInterceptor } from '@flusys/ng-auth';
|
|
288
|
+
|
|
289
|
+
provideHttpClient(
|
|
290
|
+
withInterceptors([authInterceptor, ...])
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Adds:**
|
|
295
|
+
- `Authorization: Bearer {token}` if token exists
|
|
296
|
+
- Tenant header if multi-tenant enabled
|
|
297
|
+
- `withCredentials: true` for auth endpoints (cookie support)
|
|
298
|
+
|
|
299
|
+
### tokenRefreshInterceptor
|
|
300
|
+
|
|
301
|
+
Handles 401 responses and token refresh.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { tokenRefreshInterceptor } from '@flusys/ng-auth';
|
|
305
|
+
|
|
306
|
+
provideHttpClient(
|
|
307
|
+
withInterceptors([authInterceptor, tokenRefreshInterceptor, ...])
|
|
308
|
+
)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Behavior:**
|
|
312
|
+
1. Intercepts 401 Unauthorized responses
|
|
313
|
+
2. Skips refresh for: `/auth/refresh`, `/auth/login`, `/auth/register`, `/auth/select`
|
|
314
|
+
3. Uses `TokenRefreshStateService` to queue concurrent requests during refresh
|
|
315
|
+
4. On success: updates token, retries original request
|
|
316
|
+
5. On failure: logs out, shows error, redirects to login
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Constants
|
|
321
|
+
|
|
322
|
+
### AUTH_ENDPOINTS
|
|
323
|
+
|
|
324
|
+
Centralized endpoint definitions used by interceptors.
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { AUTH_ENDPOINTS, isAuthEndpoint, isEndpoint } from '@flusys/ng-auth';
|
|
328
|
+
|
|
329
|
+
// Endpoint constants
|
|
330
|
+
AUTH_ENDPOINTS.LOGIN // '/auth/login'
|
|
331
|
+
AUTH_ENDPOINTS.LOGOUT // '/auth/logout'
|
|
332
|
+
AUTH_ENDPOINTS.REFRESH // '/auth/refresh'
|
|
333
|
+
AUTH_ENDPOINTS.REGISTER // '/auth/register'
|
|
334
|
+
AUTH_ENDPOINTS.SELECT // '/auth/select'
|
|
335
|
+
AUTH_ENDPOINTS.SWITCH_COMPANY // '/auth/switch-company'
|
|
336
|
+
|
|
337
|
+
// Helper functions
|
|
338
|
+
isAuthEndpoint(url); // Check if URL is any auth endpoint
|
|
339
|
+
isEndpoint(url, endpoint); // Check if URL matches specific endpoint
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Provider Adapters
|
|
345
|
+
|
|
346
|
+
ng-auth implements provider interfaces from ng-shared, enabling ng-iam and ng-storage to access auth data without direct dependencies.
|
|
347
|
+
|
|
348
|
+
### provideAuthProviders()
|
|
349
|
+
|
|
350
|
+
Registers provider adapters for ng-iam/ng-storage.
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
import { provideAuthProviders } from '@flusys/ng-auth';
|
|
354
|
+
|
|
355
|
+
// app.config.ts
|
|
356
|
+
providers: [
|
|
357
|
+
...provideAuthProviders()
|
|
358
|
+
]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Registers:**
|
|
362
|
+
- `USER_PROVIDER` → `AuthUserProviderAdapter` (user list for ng-iam)
|
|
363
|
+
- `COMPANY_API_PROVIDER` → `AuthCompanyApiProviderAdapter` (company list for ng-iam)
|
|
364
|
+
- `USER_PERMISSION_PROVIDER` → `AuthUserPermissionProviderAdapter` (permission queries)
|
|
365
|
+
|
|
366
|
+
### provideAuthLayoutIntegration()
|
|
367
|
+
|
|
368
|
+
Bridges ng-auth to ng-layout components.
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { provideAuthLayoutIntegration } from '@flusys/ng-auth';
|
|
372
|
+
|
|
373
|
+
// app.config.ts
|
|
374
|
+
providers: [
|
|
375
|
+
...provideAuthLayoutIntegration()
|
|
376
|
+
]
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Registers:**
|
|
380
|
+
- `LAYOUT_AUTH_STATE` → `AuthLayoutStateAdapter` (user/company/branch signals)
|
|
381
|
+
- `LAYOUT_AUTH_API` → `AuthLayoutApiAdapter` (logout, switch company, etc.)
|
|
382
|
+
|
|
383
|
+
**AuthLayoutApiAdapter Methods:**
|
|
384
|
+
```typescript
|
|
385
|
+
logOut();
|
|
386
|
+
navigateLogin(withUrl?: boolean);
|
|
387
|
+
switchCompany(companyId, branchId); // Triggers page reload
|
|
388
|
+
getUserCompanies(); // User's permitted companies
|
|
389
|
+
getCompanyBranches(companyId); // User's permitted branches
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Pages
|
|
395
|
+
|
|
396
|
+
### Authentication Pages
|
|
397
|
+
|
|
398
|
+
**LoginPageComponent** (`/auth/login`)
|
|
399
|
+
- Multi-step login: Step 1 = credentials, Step 2 = company/branch selection (if needed)
|
|
400
|
+
- Visual step indicator
|
|
401
|
+
- Auto-selects if only one company/branch available
|
|
402
|
+
- Stores returnUrl for post-login redirect
|
|
403
|
+
|
|
404
|
+
**RegisterPageComponent** (`/auth/register`)
|
|
405
|
+
- User registration with optional company creation
|
|
406
|
+
- Email/password validation
|
|
407
|
+
- Terms acceptance
|
|
408
|
+
|
|
409
|
+
### Administration Pages
|
|
410
|
+
|
|
411
|
+
**AdministrationPageComponent** (`/administration`)
|
|
412
|
+
- Tabbed interface: Users, Companies, Branches
|
|
413
|
+
- Company/Branch tabs require company feature enabled
|
|
414
|
+
|
|
415
|
+
**User Management:**
|
|
416
|
+
- `UserListComponent` - LazyLoad datatable with Edit, Delete, Permission actions
|
|
417
|
+
- `UserFormComponent` - Create/edit user form
|
|
418
|
+
|
|
419
|
+
**Company Management:**
|
|
420
|
+
- `CompanyListComponent` - Company CRUD
|
|
421
|
+
- `CompanyFormComponent` - Create/edit company
|
|
422
|
+
|
|
423
|
+
**Branch Management:**
|
|
424
|
+
- `BranchListComponent` - Branch CRUD filtered by company
|
|
425
|
+
- `BranchFormComponent` - Create/edit branch
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Dialog Components
|
|
430
|
+
|
|
431
|
+
### UserCompanyPermissionDialogComponent
|
|
432
|
+
|
|
433
|
+
Manages user-to-company assignments with checkboxes.
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
import { UserCompanyPermissionDialogComponent } from '@flusys/ng-auth';
|
|
437
|
+
|
|
438
|
+
@Component({
|
|
439
|
+
imports: [UserCompanyPermissionDialogComponent],
|
|
440
|
+
template: `
|
|
441
|
+
<lib-user-company-permission-dialog
|
|
442
|
+
[visible]="showDialog()"
|
|
443
|
+
[user]="selectedUser()"
|
|
444
|
+
(closed)="showDialog.set(false)"
|
|
445
|
+
(permissionsChanged)="reloadData()" />
|
|
446
|
+
`
|
|
447
|
+
})
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**Features:**
|
|
451
|
+
- Shows all companies with checkboxes
|
|
452
|
+
- Pre-checks assigned companies
|
|
453
|
+
- Immediate save on toggle (no Save button)
|
|
454
|
+
- Toast notifications for success/error
|
|
455
|
+
|
|
456
|
+
### UserBranchPermissionDialogComponent
|
|
457
|
+
|
|
458
|
+
Manages user-to-branch assignments with company filtering.
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
import { UserBranchPermissionDialogComponent } from '@flusys/ng-auth';
|
|
462
|
+
|
|
463
|
+
@Component({
|
|
464
|
+
imports: [UserBranchPermissionDialogComponent],
|
|
465
|
+
template: `
|
|
466
|
+
<lib-user-branch-permission-dialog
|
|
467
|
+
[visible]="showDialog()"
|
|
468
|
+
[user]="selectedUser()"
|
|
469
|
+
(closed)="showDialog.set(false)"
|
|
470
|
+
(permissionsChanged)="reloadData()" />
|
|
471
|
+
`
|
|
472
|
+
})
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Features:**
|
|
476
|
+
- Shows only companies where user has company-level permissions
|
|
477
|
+
- User selects company → branches load
|
|
478
|
+
- Immediate save on toggle
|
|
479
|
+
- Scoped to user's permitted companies
|
|
480
|
+
|
|
481
|
+
**Workflow:**
|
|
482
|
+
1. Dialog opens → Loads user's permitted companies
|
|
483
|
+
2. User selects company → Loads branches for that company
|
|
484
|
+
3. User toggles branch checkboxes → Immediately assigns/revokes
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Routes
|
|
489
|
+
|
|
490
|
+
### AUTH_ROUTES
|
|
491
|
+
|
|
492
|
+
Public authentication routes.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { AUTH_ROUTES } from '@flusys/ng-auth';
|
|
496
|
+
|
|
497
|
+
// Routes: /auth/login, /auth/register
|
|
498
|
+
{ path: 'auth', children: AUTH_ROUTES }
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### ADMINISTRATION_ROUTES
|
|
502
|
+
|
|
503
|
+
Protected administration routes.
|
|
504
|
+
|
|
505
|
+
```typescript
|
|
506
|
+
import { ADMINISTRATION_ROUTES } from '@flusys/ng-auth';
|
|
507
|
+
|
|
508
|
+
// Routes: /administration/users/*, /administration/company/*, /administration/branch/*
|
|
509
|
+
{
|
|
510
|
+
path: 'administration',
|
|
511
|
+
canActivate: [authGuard],
|
|
512
|
+
children: ADMINISTRATION_ROUTES
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Setup
|
|
519
|
+
|
|
520
|
+
### app.config.ts
|
|
521
|
+
|
|
522
|
+
```typescript
|
|
523
|
+
import { ApplicationConfig } from '@angular/core';
|
|
524
|
+
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
|
525
|
+
import { APP_CONFIG, errorCatchingInterceptor } from '@flusys/ng-core';
|
|
526
|
+
import {
|
|
527
|
+
authInterceptor,
|
|
528
|
+
tokenRefreshInterceptor,
|
|
529
|
+
provideAuthLayoutIntegration,
|
|
530
|
+
provideAuthProviders
|
|
531
|
+
} from '@flusys/ng-auth';
|
|
532
|
+
|
|
533
|
+
export const appConfig: ApplicationConfig = {
|
|
534
|
+
providers: [
|
|
535
|
+
provideHttpClient(
|
|
536
|
+
withFetch(),
|
|
537
|
+
withInterceptors([
|
|
538
|
+
authInterceptor,
|
|
539
|
+
tokenRefreshInterceptor,
|
|
540
|
+
errorCatchingInterceptor
|
|
541
|
+
])
|
|
542
|
+
),
|
|
543
|
+
{ provide: APP_CONFIG, useValue: DEFAULT_APP_CONFIG },
|
|
544
|
+
...provideAuthLayoutIntegration(),
|
|
545
|
+
...provideAuthProviders()
|
|
546
|
+
]
|
|
547
|
+
};
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### app.routes.ts
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
import { Routes } from '@angular/router';
|
|
554
|
+
import { authGuard, guestGuard, AUTH_ROUTES, ADMINISTRATION_ROUTES } from '@flusys/ng-auth';
|
|
555
|
+
|
|
556
|
+
export const routes: Routes = [
|
|
557
|
+
{ path: 'auth', children: AUTH_ROUTES },
|
|
558
|
+
{
|
|
559
|
+
path: '',
|
|
560
|
+
canActivate: [authGuard],
|
|
561
|
+
children: [
|
|
562
|
+
{ path: 'dashboard', loadComponent: () => import('./dashboard.component') },
|
|
563
|
+
{ path: 'administration', children: ADMINISTRATION_ROUTES }
|
|
564
|
+
]
|
|
565
|
+
}
|
|
566
|
+
];
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Interfaces
|
|
572
|
+
|
|
573
|
+
### Request DTOs
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
interface ILoginRequest { email: string; password: string; }
|
|
577
|
+
interface IRegistrationRequest { name: string; email: string; password: string; phone?: string; }
|
|
578
|
+
interface ISelectRequest { companyId: string; branchId?: string; sessionId: string; }
|
|
579
|
+
interface ISwitchCompanyRequest { companyId: string; branchId: string; }
|
|
580
|
+
interface IChangePasswordRequest { currentPassword: string; newPassword: string; }
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Response DTOs
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
interface ILoginResponse {
|
|
587
|
+
accessToken: string;
|
|
588
|
+
user: IUserInfo;
|
|
589
|
+
requiresSelection?: boolean;
|
|
590
|
+
sessionId?: string;
|
|
591
|
+
companies?: ICompanyWithBranches[];
|
|
592
|
+
expiresAt?: number;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
interface ICompanySelectionResponse {
|
|
596
|
+
accessToken: string;
|
|
597
|
+
user: IUserInfo;
|
|
598
|
+
company: ICompanyInfo;
|
|
599
|
+
branch: IBranchInfo;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
interface IMeResponse extends IUserInfo {}
|
|
603
|
+
interface IRefreshTokenResponse { accessToken: string; }
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Entity DTOs
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
interface IUser {
|
|
610
|
+
id: string;
|
|
611
|
+
name: string;
|
|
612
|
+
email: string;
|
|
613
|
+
phone?: string;
|
|
614
|
+
profilePictureId?: string;
|
|
615
|
+
isActive: boolean;
|
|
616
|
+
isEmailVerified: boolean;
|
|
617
|
+
isPhoneVerified: boolean;
|
|
618
|
+
lastLoginAt?: Date;
|
|
619
|
+
createdAt: Date;
|
|
620
|
+
updatedAt: Date;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
interface ICompany {
|
|
624
|
+
id: string;
|
|
625
|
+
name: string;
|
|
626
|
+
slug: string;
|
|
627
|
+
logoId?: string;
|
|
628
|
+
address?: string;
|
|
629
|
+
phone?: string;
|
|
630
|
+
email?: string;
|
|
631
|
+
website?: string;
|
|
632
|
+
isActive: boolean;
|
|
633
|
+
createdAt: Date;
|
|
634
|
+
updatedAt: Date;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
interface IBranch {
|
|
638
|
+
id: string;
|
|
639
|
+
name: string;
|
|
640
|
+
slug: string;
|
|
641
|
+
companyId: string;
|
|
642
|
+
parentId?: string;
|
|
643
|
+
logoId?: string;
|
|
644
|
+
address?: string;
|
|
645
|
+
phone?: string;
|
|
646
|
+
email?: string;
|
|
647
|
+
isActive: boolean;
|
|
648
|
+
createdAt: Date;
|
|
649
|
+
updatedAt: Date;
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Permission DTOs
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
interface IPermissionItem { targetId: string; isAdd: boolean; }
|
|
657
|
+
interface IAssignUserCompanyRequest { userId: string; items: IPermissionItem[]; }
|
|
658
|
+
interface IAssignUserBranchRequest { userId: string; items: IPermissionItem[]; }
|
|
659
|
+
interface IBatchOperationResponse { success: boolean; message: string; }
|
|
660
|
+
interface IUserCompanyPermission { id: string; userId: string; companyId: string; companyName: string; companySlug: string; }
|
|
661
|
+
interface IUserBranchPermission { id: string; userId: string; branchId: string; branchName: string; branchSlug: string; companyId: string; }
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
## Best Practices
|
|
667
|
+
|
|
668
|
+
### Component Organization
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
projects/ng-auth/
|
|
672
|
+
├── components/ ← Shared dialogs
|
|
673
|
+
│ ├── user-company-permission-dialog.component.ts
|
|
674
|
+
│ └── user-branch-permission-dialog.component.ts
|
|
675
|
+
├── pages/ ← Route-specific pages
|
|
676
|
+
│ ├── login/
|
|
677
|
+
│ ├── register/
|
|
678
|
+
│ └── users/
|
|
679
|
+
├── services/
|
|
680
|
+
├── guards/
|
|
681
|
+
├── interceptors/
|
|
682
|
+
└── adapters/
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Rule:** Dialog components go in `components/`, page components go in `pages/`.
|
|
686
|
+
|
|
687
|
+
### Permission Filtering Pattern
|
|
688
|
+
|
|
689
|
+
When building permission UI, filter by user's permitted scope:
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
// ✅ CORRECT - Filter by user's permitted companies
|
|
693
|
+
const response = await firstValueFrom(permApi.getUserCompanies(userId));
|
|
694
|
+
const userCompanies = response.data;
|
|
695
|
+
|
|
696
|
+
// ❌ WRONG - Don't show all companies
|
|
697
|
+
const allCompanies = await companyApi.fetchList(); // Security risk
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Token Management
|
|
701
|
+
|
|
702
|
+
- Use httpOnly cookies for refresh tokens (backend)
|
|
703
|
+
- Access tokens stored in memory via AuthStateService
|
|
704
|
+
- TokenRefreshInterceptor handles automatic refresh
|
|
705
|
+
- Auto-persistence for company/branch selection
|
|
706
|
+
|
|
707
|
+
### Multi-Company Flow
|
|
708
|
+
|
|
709
|
+
1. Login → Check if `requiresSelection: true`
|
|
710
|
+
2. If yes → Show Step 2 (company/branch selection)
|
|
711
|
+
3. Auto-select if only one company/branch
|
|
712
|
+
4. Save selection → Navigate to dashboard
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Common Issues
|
|
717
|
+
|
|
718
|
+
### Guards Not Working
|
|
719
|
+
|
|
720
|
+
**Problem:** Routes accessible without authentication.
|
|
721
|
+
|
|
722
|
+
**Solution:** Ensure authGuard is on parent route and session is restored:
|
|
723
|
+
```typescript
|
|
724
|
+
{
|
|
725
|
+
path: '',
|
|
726
|
+
canActivate: [authGuard], // Parent guard
|
|
727
|
+
children: [...]
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
### Token Not Sent
|
|
732
|
+
|
|
733
|
+
**Problem:** 401 on all requests.
|
|
734
|
+
|
|
735
|
+
**Solution:** Ensure interceptor order is correct:
|
|
736
|
+
```typescript
|
|
737
|
+
withInterceptors([
|
|
738
|
+
authInterceptor, // First: adds token
|
|
739
|
+
tokenRefreshInterceptor, // Second: handles 401
|
|
740
|
+
errorCatchingInterceptor // Last: shows errors
|
|
741
|
+
])
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
### Layout Not Showing User
|
|
745
|
+
|
|
746
|
+
**Problem:** User info missing from topbar.
|
|
747
|
+
|
|
748
|
+
**Solution:** Ensure layout integration is provided:
|
|
749
|
+
```typescript
|
|
750
|
+
providers: [
|
|
751
|
+
...provideAuthLayoutIntegration()
|
|
752
|
+
]
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Company Switcher Shows All Companies
|
|
756
|
+
|
|
757
|
+
**Problem:** Switcher shows companies user doesn't have access to.
|
|
758
|
+
|
|
759
|
+
**Solution:** `AuthLayoutApiAdapter.getUserCompanies()` uses `UserPermissionApiService.getUserCompanies(userId)` internally - ensure user has company permissions assigned.
|
|
760
|
+
|
|
761
|
+
---
|
|
762
|
+
|
|
763
|
+
## See Also
|
|
764
|
+
|
|
765
|
+
- [CORE-GUIDE.md](./CORE-GUIDE.md) - APP_CONFIG configuration
|
|
766
|
+
- [SHARED-GUIDE.md](./SHARED-GUIDE.md) - Provider interfaces
|
|
767
|
+
- [LAYOUT-GUIDE.md](./LAYOUT-GUIDE.md) - Layout integration
|
|
768
|
+
- [IAM-GUIDE.md](./IAM-GUIDE.md) - Permission management
|
|
769
|
+
- [../FLUSYS_NEST/docs/AUTH-GUIDE.md](../../FLUSYS_NEST/docs/AUTH-GUIDE.md) - Backend auth module
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
**Last Updated:** 2026-02-07
|
|
774
|
+
**Package Version:** 1.0.1
|
|
775
|
+
**Angular Version:** 21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/ng-auth",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
4
|
"description": "Authentication module for FLUSYS Angular applications",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"peerDependencies": {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"@angular/core": "^21.0.0",
|
|
9
9
|
"@angular/forms": "^21.0.0",
|
|
10
10
|
"@angular/router": "^21.0.0",
|
|
11
|
-
"@flusys/ng-core": "^0.1.0-beta.
|
|
12
|
-
"@flusys/ng-layout": "^0.1.0-beta.
|
|
13
|
-
"@flusys/ng-shared": "^0.1.0-beta.
|
|
11
|
+
"@flusys/ng-core": "^0.1.0-beta.3",
|
|
12
|
+
"@flusys/ng-layout": "^0.1.0-beta.3",
|
|
13
|
+
"@flusys/ng-shared": "^0.1.0-beta.3",
|
|
14
14
|
"@primeuix/themes": "^1.0.0",
|
|
15
15
|
"primeicons": "^7.0.0",
|
|
16
16
|
"primeng": "^21.0.0"
|