@postxl/generators 1.11.6 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-view/model-view-service.generator.js +4 -1
- package/dist/backend-view/model-view-service.generator.js.map +1 -1
- package/dist/backend-view/template/query.utils.test.ts +34 -0
- package/dist/backend-view/template/query.utils.ts +89 -0
- package/dist/frontend-admin/admin.generator.d.ts +9 -0
- package/dist/frontend-admin/admin.generator.js +19 -0
- package/dist/frontend-admin/admin.generator.js.map +1 -1
- package/dist/frontend-admin/generators/admin-sidebar.generator.js +1 -1
- package/dist/frontend-admin/generators/audit-log-sidebar.generator.js +107 -113
- package/dist/frontend-admin/generators/audit-log-sidebar.generator.js.map +1 -1
- package/dist/frontend-admin/generators/comment-sidebar.generator.d.ts +9 -0
- package/dist/frontend-admin/generators/comment-sidebar.generator.js +246 -0
- package/dist/frontend-admin/generators/comment-sidebar.generator.js.map +1 -0
- package/dist/frontend-admin/generators/detail-sidebar.generator.d.ts +9 -0
- package/dist/frontend-admin/generators/detail-sidebar.generator.js +148 -0
- package/dist/frontend-admin/generators/detail-sidebar.generator.js.map +1 -0
- package/dist/frontend-admin/generators/model-admin-page.generator.js +40 -6
- package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
- package/dist/frontend-core/frontend.generator.js +2 -1
- package/dist/frontend-core/frontend.generator.js.map +1 -1
- package/dist/frontend-core/template/docs/LOGIN_PROCESS.d2 +372 -0
- package/dist/frontend-core/template/docs/LOGIN_PROCESS.md +214 -0
- package/dist/frontend-core/template/docs/LOGIN_PROCESS.svg +914 -0
- package/dist/frontend-core/template/src/components/admin/table-filter.stories.tsx +265 -0
- package/dist/frontend-core/template/src/components/admin/table-filter.tsx +224 -128
- package/dist/frontend-core/template/src/components/admin/table-view-panel.tsx +159 -0
- package/dist/frontend-core/template/src/context-providers/auth-context-provider.tsx +54 -43
- package/dist/frontend-core/template/src/hooks/use-table-view-config.tsx +249 -0
- package/dist/frontend-core/template/src/pages/dashboard/dashboard.page.tsx +5 -5
- package/dist/frontend-core/template/src/pages/error/auth-error.page.tsx +37 -0
- package/dist/frontend-core/template/src/pages/error/default-error.page.tsx +24 -40
- package/dist/frontend-core/template/src/pages/error/not-found-error.page.tsx +8 -10
- package/dist/frontend-core/template/src/pages/login/login.page.tsx +39 -13
- package/dist/frontend-core/template/src/pages/unauthorized/unauthorized.page.tsx +2 -2
- package/dist/frontend-core/template/src/routes/_auth-routes.tsx +21 -8
- package/dist/frontend-core/template/src/routes/auth-error.tsx +19 -0
- package/dist/frontend-core/template/vite.config.ts +5 -0
- package/dist/frontend-core/types/hook.d.ts +1 -1
- package/dist/frontend-tables/generators/model-table.generator.js +30 -5
- package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
- package/dist/types/template/query.types.ts +19 -1
- package/package.json +5 -5
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# SteerEx Login Process Flow
|
|
2
|
+
direction: down
|
|
3
|
+
|
|
4
|
+
title: {
|
|
5
|
+
label: SteerEx Authentication Flow
|
|
6
|
+
near: top-center
|
|
7
|
+
shape: text
|
|
8
|
+
style: {
|
|
9
|
+
font-size: 24
|
|
10
|
+
bold: true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# ============================================
|
|
15
|
+
# Entry Point
|
|
16
|
+
# ============================================
|
|
17
|
+
user: {
|
|
18
|
+
label: User
|
|
19
|
+
shape: person
|
|
20
|
+
style.fill: "#e3f2fd"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# ============================================
|
|
24
|
+
# Login Phase
|
|
25
|
+
# ============================================
|
|
26
|
+
login_phase: Login Phase {
|
|
27
|
+
style.fill: "#fafafa"
|
|
28
|
+
style.stroke: "#bdbdbd"
|
|
29
|
+
style.border-radius: 8
|
|
30
|
+
|
|
31
|
+
index_route: {
|
|
32
|
+
label: |md
|
|
33
|
+
**/** (Index Route)
|
|
34
|
+
Entry point
|
|
35
|
+
|
|
|
36
|
+
style.fill: "#e3f2fd"
|
|
37
|
+
style.border-radius: 6
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
login_page: {
|
|
41
|
+
label: |md
|
|
42
|
+
**/login**
|
|
43
|
+
LoginPage component
|
|
44
|
+
|
|
|
45
|
+
style.fill: "#fff3e0"
|
|
46
|
+
style.border-radius: 6
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
index_route -> login_page: Redirect
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# ============================================
|
|
53
|
+
# Keycloak Authentication
|
|
54
|
+
# ============================================
|
|
55
|
+
keycloak_phase: Keycloak Authentication {
|
|
56
|
+
style.fill: "#f3e5f5"
|
|
57
|
+
style.stroke: "#ce93d8"
|
|
58
|
+
style.border-radius: 8
|
|
59
|
+
|
|
60
|
+
kc_init: {
|
|
61
|
+
label: |md
|
|
62
|
+
**Keycloak Init**
|
|
63
|
+
check-sso mode
|
|
64
|
+
loginState = 'inProgress'
|
|
65
|
+
|
|
|
66
|
+
style.fill: "#e1bee7"
|
|
67
|
+
style.border-radius: 6
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
init_result: {
|
|
71
|
+
shape: diamond
|
|
72
|
+
label: |md
|
|
73
|
+
Init
|
|
74
|
+
result?
|
|
75
|
+
|
|
|
76
|
+
style.fill: "#fff9c4"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
sso_check: {
|
|
80
|
+
shape: diamond
|
|
81
|
+
label: |md
|
|
82
|
+
SSO
|
|
83
|
+
Session?
|
|
84
|
+
|
|
|
85
|
+
style.fill: "#fff9c4"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
kc_login: {
|
|
89
|
+
label: |md
|
|
90
|
+
**Keycloak Login**
|
|
91
|
+
User enters credentials
|
|
92
|
+
loginState = 'loggedOut'
|
|
93
|
+
|
|
|
94
|
+
style.fill: "#ffccbc"
|
|
95
|
+
style.border-radius: 6
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
silent_token: {
|
|
99
|
+
label: |md
|
|
100
|
+
**Silent Token**
|
|
101
|
+
Via iframe
|
|
102
|
+
loginState = 'loggedIn'
|
|
103
|
+
|
|
|
104
|
+
style.fill: "#c8e6c9"
|
|
105
|
+
style.border-radius: 6
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
kc_init -> init_result
|
|
109
|
+
init_result -> sso_check: Success
|
|
110
|
+
init_result -> errors.auth_error_init: "Failure → errorInit"
|
|
111
|
+
sso_check -> kc_login: No
|
|
112
|
+
sso_check -> silent_token: Yes
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# ============================================
|
|
116
|
+
# User Data & Authorization
|
|
117
|
+
# ============================================
|
|
118
|
+
auth_phase: User Data & Authorization {
|
|
119
|
+
style.fill: "#e8f5e9"
|
|
120
|
+
style.stroke: "#81c784"
|
|
121
|
+
style.border-radius: 8
|
|
122
|
+
|
|
123
|
+
load_user: {
|
|
124
|
+
label: |md
|
|
125
|
+
**Load User Data**
|
|
126
|
+
trpc.viewer.viewer
|
|
127
|
+
(enabled when loggedIn)
|
|
128
|
+
|
|
|
129
|
+
style.fill: "#c8e6c9"
|
|
130
|
+
style.border-radius: 6
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
load_check: {
|
|
134
|
+
shape: diamond
|
|
135
|
+
label: |md
|
|
136
|
+
Load
|
|
137
|
+
success?
|
|
138
|
+
|
|
|
139
|
+
style.fill: "#fff9c4"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
role_check: {
|
|
143
|
+
shape: diamond
|
|
144
|
+
label: |md
|
|
145
|
+
Has
|
|
146
|
+
'unauthorized'
|
|
147
|
+
role?
|
|
148
|
+
|
|
|
149
|
+
style.fill: "#fff9c4"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
load_user -> load_check
|
|
153
|
+
load_check -> role_check: Success
|
|
154
|
+
load_check -> errors.auth_error_load: "Failure → errorLoadUser"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# ============================================
|
|
158
|
+
# Error States
|
|
159
|
+
# ============================================
|
|
160
|
+
errors: Error States {
|
|
161
|
+
style.fill: "#ffebee"
|
|
162
|
+
style.stroke: "#ef9a9a"
|
|
163
|
+
style.border-radius: 8
|
|
164
|
+
|
|
165
|
+
auth_error_init: {
|
|
166
|
+
label: |md
|
|
167
|
+
**/auth-error**
|
|
168
|
+
?type=errorInit
|
|
169
|
+
|
|
|
170
|
+
style.fill: "#ffcdd2"
|
|
171
|
+
style.border-radius: 6
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
auth_error_load: {
|
|
175
|
+
label: |md
|
|
176
|
+
**/auth-error**
|
|
177
|
+
?type=errorLoadUser
|
|
178
|
+
|
|
|
179
|
+
style.fill: "#ffcdd2"
|
|
180
|
+
style.border-radius: 6
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
auth_error_token: {
|
|
184
|
+
label: |md
|
|
185
|
+
**/auth-error**
|
|
186
|
+
?type=errorTokenRefresh
|
|
187
|
+
|
|
|
188
|
+
style.fill: "#ffcdd2"
|
|
189
|
+
style.border-radius: 6
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
unauthorized: {
|
|
193
|
+
label: |md
|
|
194
|
+
**/unauthorized**
|
|
195
|
+
|
|
|
196
|
+
style.fill: "#ffcdd2"
|
|
197
|
+
style.border-radius: 6
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
no_programs: {
|
|
201
|
+
label: |md
|
|
202
|
+
**Error**
|
|
203
|
+
No Programs
|
|
204
|
+
|
|
|
205
|
+
style.fill: "#ffcdd2"
|
|
206
|
+
style.border-radius: 6
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# ============================================
|
|
211
|
+
# Protected Routes
|
|
212
|
+
# ============================================
|
|
213
|
+
protected_phase: Protected Routes {
|
|
214
|
+
style.fill: "#e3f2fd"
|
|
215
|
+
style.stroke: "#64b5f6"
|
|
216
|
+
style.border-radius: 8
|
|
217
|
+
|
|
218
|
+
auth_guard: {
|
|
219
|
+
label: |md
|
|
220
|
+
**/_auth-routes**
|
|
221
|
+
AuthGuard component
|
|
222
|
+
(renders when loggedIn)
|
|
223
|
+
|
|
|
224
|
+
style.fill: "#bbdefb"
|
|
225
|
+
style.border-radius: 6
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
load_programs: {
|
|
229
|
+
label: |md
|
|
230
|
+
**Load Programs**
|
|
231
|
+
useUserPrograms()
|
|
232
|
+
|
|
|
233
|
+
style.fill: "#b3e5fc"
|
|
234
|
+
style.border-radius: 6
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
programs_check: {
|
|
238
|
+
shape: diamond
|
|
239
|
+
label: |md
|
|
240
|
+
Programs
|
|
241
|
+
available?
|
|
242
|
+
|
|
|
243
|
+
style.fill: "#fff9c4"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
select_program: {
|
|
247
|
+
label: |md
|
|
248
|
+
**Select Program**
|
|
249
|
+
localStorage or first
|
|
250
|
+
|
|
|
251
|
+
style.fill: "#b3e5fc"
|
|
252
|
+
style.border-radius: 6
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
set_theme: {
|
|
256
|
+
label: |md
|
|
257
|
+
**Set Theme**
|
|
258
|
+
program.themeKey
|
|
259
|
+
|
|
|
260
|
+
style.fill: "#b3e5fc"
|
|
261
|
+
style.border-radius: 6
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
auth_guard -> load_programs
|
|
265
|
+
load_programs -> programs_check
|
|
266
|
+
programs_check -> select_program: Yes
|
|
267
|
+
select_program -> set_theme
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# ============================================
|
|
271
|
+
# Dashboard Redirects
|
|
272
|
+
# ============================================
|
|
273
|
+
dashboard_phase: Dashboard Redirects {
|
|
274
|
+
style.fill: "#fff8e1"
|
|
275
|
+
style.stroke: "#ffcc80"
|
|
276
|
+
style.border-radius: 8
|
|
277
|
+
|
|
278
|
+
program_index: {
|
|
279
|
+
label: |md
|
|
280
|
+
**/$programId** (index)
|
|
281
|
+
Redirect to deliverable
|
|
282
|
+
|
|
|
283
|
+
style.fill: "#ffe0b2"
|
|
284
|
+
style.border-radius: 6
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
deliverable_index: {
|
|
288
|
+
label: |md
|
|
289
|
+
**/$programId/$deliverableId** (index)
|
|
290
|
+
Default to 'overview' tab
|
|
291
|
+
|
|
|
292
|
+
style.fill: "#ffe0b2"
|
|
293
|
+
style.border-radius: 6
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# ============================================
|
|
298
|
+
# Final Destination
|
|
299
|
+
# ============================================
|
|
300
|
+
final: {
|
|
301
|
+
label: |md
|
|
302
|
+
**/dashboard/$programId/$deliverableId/overview**
|
|
303
|
+
|
|
304
|
+
Application Ready
|
|
305
|
+
|
|
|
306
|
+
style.fill: "#81c784"
|
|
307
|
+
style.stroke: "#43a047"
|
|
308
|
+
style.stroke-width: 3
|
|
309
|
+
style.border-radius: 8
|
|
310
|
+
style.font-color: "#1b5e20"
|
|
311
|
+
style.bold: true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# ============================================
|
|
315
|
+
# Main Flow Connections
|
|
316
|
+
# ============================================
|
|
317
|
+
user -> login_phase.index_route: Opens app
|
|
318
|
+
|
|
319
|
+
login_phase.login_page -> keycloak_phase.kc_init
|
|
320
|
+
|
|
321
|
+
keycloak_phase.kc_login -> auth_phase.load_user: "Token received → loggedIn"
|
|
322
|
+
keycloak_phase.silent_token -> auth_phase.load_user
|
|
323
|
+
|
|
324
|
+
auth_phase.role_check -> errors.unauthorized: Yes
|
|
325
|
+
auth_phase.role_check -> protected_phase.auth_guard: No
|
|
326
|
+
|
|
327
|
+
protected_phase.programs_check -> errors.no_programs: No
|
|
328
|
+
protected_phase.set_theme -> dashboard_phase.program_index
|
|
329
|
+
|
|
330
|
+
dashboard_phase.program_index -> dashboard_phase.deliverable_index: Redirect
|
|
331
|
+
dashboard_phase.deliverable_index -> final: Redirect
|
|
332
|
+
|
|
333
|
+
# Token refresh error (runtime)
|
|
334
|
+
protected_phase.auth_guard -> errors.auth_error_token: "Token refresh fails → errorTokenRefresh" {
|
|
335
|
+
style.stroke-dash: 5
|
|
336
|
+
style.stroke: "#ef9a9a"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
# ============================================
|
|
340
|
+
# Legend
|
|
341
|
+
# ============================================
|
|
342
|
+
legend: Legend {
|
|
343
|
+
style.fill: "#fafafa"
|
|
344
|
+
style.stroke: "#e0e0e0"
|
|
345
|
+
style.border-radius: 8
|
|
346
|
+
near: bottom-right
|
|
347
|
+
|
|
348
|
+
l1: Login {
|
|
349
|
+
style.fill: "#fff3e0"
|
|
350
|
+
style.border-radius: 4
|
|
351
|
+
}
|
|
352
|
+
l2: Keycloak {
|
|
353
|
+
style.fill: "#e1bee7"
|
|
354
|
+
style.border-radius: 4
|
|
355
|
+
}
|
|
356
|
+
l3: Auth {
|
|
357
|
+
style.fill: "#c8e6c9"
|
|
358
|
+
style.border-radius: 4
|
|
359
|
+
}
|
|
360
|
+
l4: Routes {
|
|
361
|
+
style.fill: "#bbdefb"
|
|
362
|
+
style.border-radius: 4
|
|
363
|
+
}
|
|
364
|
+
l5: Dashboard {
|
|
365
|
+
style.fill: "#ffe0b2"
|
|
366
|
+
style.border-radius: 4
|
|
367
|
+
}
|
|
368
|
+
l6: Error {
|
|
369
|
+
style.fill: "#ffcdd2"
|
|
370
|
+
style.border-radius: 4
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Login Process Documentation
|
|
2
|
+
|
|
3
|
+
This document describes the authentication flow in SteerEx, from initial page load to the dashboard view.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
SteerEx uses **Keycloak** as the identity provider for authentication. The frontend uses the `keycloak-js` adapter with a **check-sso** strategy, which checks for an existing session without forcing a login prompt.
|
|
8
|
+
|
|
9
|
+
Authentication state is managed through a single `loginState` value that can be one of:
|
|
10
|
+
- `inProgress` — Keycloak initialization is underway
|
|
11
|
+
- `loggedIn` — User is authenticated
|
|
12
|
+
- `loggedOut` — User is not authenticated (triggers Keycloak login)
|
|
13
|
+
- `errorInit` — Keycloak initialization failed
|
|
14
|
+
- `errorLoadUser` — User data could not be loaded from the backend
|
|
15
|
+
- `errorTokenRefresh` — Token refresh failed (session expired)
|
|
16
|
+
- `errorNoProgram` — User has no programs or program loading failed (used by `AuthErrorPage`, not set on `loginState`)
|
|
17
|
+
|
|
18
|
+
## Key Components
|
|
19
|
+
|
|
20
|
+
| Component | File | Purpose |
|
|
21
|
+
| ----------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
|
22
|
+
| AuthProvider | `src/context-providers/auth-context-provider.tsx` | Manages `loginState`, Keycloak initialization, token refresh |
|
|
23
|
+
| LoginPage | `src/pages/login/login.page.tsx` | Handles login flow, error redirects, and post-auth navigation |
|
|
24
|
+
| AuthGuard | `src/routes/_auth-routes.tsx` | Protects routes, loads user programs, sets theme |
|
|
25
|
+
| AuthErrorPage | `src/pages/error/auth-error.page.tsx` | Displays auth error messages with "Try Again" and "Login Again" |
|
|
26
|
+
| Program Wrapper | `src/routes/_auth-routes/$programId.tsx` | DashboardWrapperPage layout for program routes |
|
|
27
|
+
| Program Index | `src/routes/_auth-routes/$programId/index.tsx` | Redirects to first deliverable of program, or shows error |
|
|
28
|
+
| Deliverable Index | `src/routes/_auth-routes/$programId/$deliverableId/index.lazy.tsx` | Redirects to default tab (workstream) |
|
|
29
|
+
| Auth Error Route | `src/routes/auth-error.tsx` | `/auth-error` route with Zod-validated `type` search param |
|
|
30
|
+
| Index Route | `src/routes/index.tsx` | Entry point, redirects to `/login` |
|
|
31
|
+
|
|
32
|
+
## Authentication Flow
|
|
33
|
+
|
|
34
|
+
### Step 1: Initial Page Load
|
|
35
|
+
|
|
36
|
+
When a user visits the application:
|
|
37
|
+
|
|
38
|
+
1. **Index route (`/`)** immediately redirects to `/login`
|
|
39
|
+
2. **AuthProvider** initializes Keycloak with `check-sso` mode, setting `loginState` to `inProgress`
|
|
40
|
+
3. Keycloak checks for existing SSO session via silent iframe (`/silent-check-sso.html`)
|
|
41
|
+
4. On success: `loginState` becomes `loggedIn` (if authenticated) or `loggedOut` (if not)
|
|
42
|
+
5. On failure: `loginState` becomes `errorInit`
|
|
43
|
+
|
|
44
|
+
### Step 2: Login Page Logic
|
|
45
|
+
|
|
46
|
+
The `LoginPage` component is a non-visual component that reacts to `loginState` changes via a `useEffect`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
┌───────────────────────────────────────────────────────────────────────┐
|
|
50
|
+
│ LoginPage Effect (runs on loginState changes) │
|
|
51
|
+
├───────────────────────────────────────────────────────────────────────┤
|
|
52
|
+
│ 1. if (loginState === 'inProgress') → wait (show "Signing In...") │
|
|
53
|
+
│ 2. if (loginState === 'loggedOut') → call login() → Keycloak │
|
|
54
|
+
│ 3. if (loginState is an error) → navigate to /auth-error?type=... │
|
|
55
|
+
│ 4. if (loggedIn && !viewerData) → wait for user data to load │
|
|
56
|
+
│ 5. if (viewerData.roles includes 'unauthorized') → /unauthorized │
|
|
57
|
+
│ 6. else → sanitize redirectUrl, then redirect to: │
|
|
58
|
+
│ redirectUrl (if safe) → /$programId → /auth-error?errorNoProgram │
|
|
59
|
+
└───────────────────────────────────────────────────────────────────────┘
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Redirect URL sanitization (step 6):** The `getSafeRedirectUrl()` helper rejects any URL that doesn't start with `/`, starts with `//`, or contains `://`. This prevents open-redirect attacks via crafted `redirectUrl` search params (e.g. `//evil.com`, `https://evil.com`).
|
|
63
|
+
|
|
64
|
+
**File:** [src/pages/login/login.page.tsx](../src/pages/login/login.page.tsx)
|
|
65
|
+
|
|
66
|
+
### Step 3: User Data Loading (AuthProvider)
|
|
67
|
+
|
|
68
|
+
After Keycloak authentication succeeds (`loginState === 'loggedIn'`):
|
|
69
|
+
|
|
70
|
+
1. **AuthProvider** fetches user data via `trpc.viewer.viewer` (enabled only when `loginState === 'loggedIn'`)
|
|
71
|
+
2. Backend returns: `{ user, userRoles, programs }`
|
|
72
|
+
3. User data is stored in auth context as `viewerData`
|
|
73
|
+
4. If fetch fails or returns an error: `loginState` is set to `errorLoadUser`
|
|
74
|
+
|
|
75
|
+
**File:** [src/context-providers/auth-context-provider.tsx](../src/context-providers/auth-context-provider.tsx)
|
|
76
|
+
|
|
77
|
+
### Step 4: Auth Route Guard
|
|
78
|
+
|
|
79
|
+
When accessing protected routes (`/_auth-routes/*`):
|
|
80
|
+
|
|
81
|
+
1. **Redirect effect**: If `loginState === 'loggedOut'` → navigate to `/login?redirectUrl=...` (preserves current URL for post-login redirect)
|
|
82
|
+
2. **Authentication gate**: If `loginState !== 'loggedIn'` → render nothing (`null`). This check runs before any data-dependent rendering to prevent flashing error/loading UI while the redirect effect navigates unauthenticated users to `/login`.
|
|
83
|
+
3. **Program loading**: Loads user programs via `useUserPrograms()`
|
|
84
|
+
4. **Error handling**: If program fetch fails (`isProgramError`) or user has no programs (`isLoaded && userPrograms.length === 0`) → render `<AuthErrorPage errorType="errorNoProgram" />` directly as a component (not a route redirect, because `isLoaded` becomes `true` two render cycles before `userPrograms` is populated)
|
|
85
|
+
5. **Program selection**:
|
|
86
|
+
- Check localStorage for saved `programId`
|
|
87
|
+
- If found and valid → use it
|
|
88
|
+
- Otherwise → use first program from user's program list
|
|
89
|
+
6. **Theme setup**: Set brand theme via `setBrand(program.themeKey)`
|
|
90
|
+
7. **Render**: `<CommandPalette />` + `<Outlet />` for child routes
|
|
91
|
+
|
|
92
|
+
**File:** [src/routes/\_auth-routes.tsx](../src/routes/_auth-routes.tsx)
|
|
93
|
+
|
|
94
|
+
### Step 5: Program & Deliverable Redirects
|
|
95
|
+
|
|
96
|
+
After the auth guard renders successfully, index routes cascade to the final view:
|
|
97
|
+
|
|
98
|
+
#### `/$programId` (index)
|
|
99
|
+
|
|
100
|
+
1. Waits for `viewerData` and programs to load
|
|
101
|
+
2. If no programs → renders `<AuthErrorPage errorType="errorNoProgram" />` directly
|
|
102
|
+
3. Gets `rootDeliverableId` from `viewerData.programs[programId]`
|
|
103
|
+
4. Redirects to `/$programId/$deliverableId/workstream` via `<Navigate>`
|
|
104
|
+
|
|
105
|
+
**File:** [src/routes/\_auth-routes/$programId/index.tsx](../src/routes/_auth-routes/$programId/index.tsx)
|
|
106
|
+
|
|
107
|
+
#### `/$programId/$deliverableId` (index)
|
|
108
|
+
|
|
109
|
+
1. Gets `programId` and `deliverableId` from route params
|
|
110
|
+
2. Redirects to `/$programId/$deliverableId/workstream` via `<Navigate>`
|
|
111
|
+
|
|
112
|
+
**File:** [src/routes/\_auth-routes/$programId/$deliverableId/index.lazy.tsx](../src/routes/_auth-routes/$programId/$deliverableId/index.lazy.tsx)
|
|
113
|
+
|
|
114
|
+
## Auth Error Page
|
|
115
|
+
|
|
116
|
+
The `AuthErrorPage` component handles all authentication and program error states. It is used in two ways:
|
|
117
|
+
|
|
118
|
+
1. **As a routed page** at `/auth-error?type=...` — the `type` search param is validated with `z.enum(authErrorTypes)`
|
|
119
|
+
2. **As an inline component** rendered directly by `AuthGuard` and `ProgramRedirect` when programs can't be loaded
|
|
120
|
+
|
|
121
|
+
The page displays an error message based on the error type and provides two actions:
|
|
122
|
+
|
|
123
|
+
| Button | Action |
|
|
124
|
+
| ------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
125
|
+
| **Try Again** | Full page reload via `<a href="/">` — reinitializes all auth state from scratch |
|
|
126
|
+
| **Login Again** | Calls `logout({ redirectUri: window.location.origin })` — destroys the Keycloak SSO session, then redirects to app root which triggers the full login flow, showing the Keycloak login page |
|
|
127
|
+
|
|
128
|
+
The `logout()` function accepts an optional `{ redirectUri }` parameter that is forwarded to `keycloak.logout()`. Passing `window.location.origin` ensures Keycloak redirects back to the app root after session destruction, rather than back to `/auth-error` (which could cause a loop).
|
|
129
|
+
|
|
130
|
+
**Files:**
|
|
131
|
+
- [src/pages/error/auth-error.page.tsx](../src/pages/error/auth-error.page.tsx)
|
|
132
|
+
- [src/routes/auth-error.tsx](../src/routes/auth-error.tsx)
|
|
133
|
+
|
|
134
|
+
## Complete Flow Diagram
|
|
135
|
+
|
|
136
|
+

|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
Authentication is configured via environment variables:
|
|
141
|
+
|
|
142
|
+
| Variable | Description |
|
|
143
|
+
| -------------------------------- | ---------------------------------------------- |
|
|
144
|
+
| `VITE_AUTH` | Enable/disable authentication (`true`/`false`) |
|
|
145
|
+
| `VITE_PUBLIC_KEYCLOAK_URL` | Keycloak server URL |
|
|
146
|
+
| `VITE_PUBLIC_KEYCLOAK_REALM` | Keycloak realm name |
|
|
147
|
+
| `VITE_PUBLIC_KEYCLOAK_CLIENT_ID` | Keycloak client ID |
|
|
148
|
+
| `VITE_PUBLIC_CHECK_LOGIN_IFRAME` | Enable/disable SSO iframe check |
|
|
149
|
+
|
|
150
|
+
### No-Auth Mode
|
|
151
|
+
|
|
152
|
+
When `VITE_AUTH=false`:
|
|
153
|
+
|
|
154
|
+
- `loginState` is set directly to `loggedIn`
|
|
155
|
+
- Keycloak initialization is skipped
|
|
156
|
+
- Useful for local development: `pnpm run dev:noAuth`
|
|
157
|
+
|
|
158
|
+
## Token Management
|
|
159
|
+
|
|
160
|
+
- **Token refresh**: Every 5 seconds, checks if token expires within 60 seconds (only when `loginState === 'loggedIn'`)
|
|
161
|
+
- **Token storage**: Bearer token stored in memory via `setAuthToken()`
|
|
162
|
+
- **Token header**: `Authorization: Bearer<token>` added to API requests
|
|
163
|
+
|
|
164
|
+
## Error Handling
|
|
165
|
+
|
|
166
|
+
Errors set `loginState` to an error type. The `LoginPage` detects error states and navigates to `/auth-error?type=<errorType>`. The `errorNoProgram` type is special — it is not set on `loginState` but is used directly by components that render `<AuthErrorPage>` inline.
|
|
167
|
+
|
|
168
|
+
| Scenario | `loginState` | Behavior |
|
|
169
|
+
| ---------------------------- | ------------------- | --------------------------------------------------------------------- |
|
|
170
|
+
| Keycloak init fails | `errorInit` | LoginPage redirects to `/auth-error?type=errorInit` |
|
|
171
|
+
| User data fetch fails | `errorLoadUser` | LoginPage redirects to `/auth-error?type=errorLoadUser` |
|
|
172
|
+
| User has `unauthorized` role | (stays `loggedIn`) | LoginPage redirects to `/unauthorized` |
|
|
173
|
+
| Token refresh fails | `errorTokenRefresh` | LoginPage redirects to `/auth-error?type=errorTokenRefresh` |
|
|
174
|
+
| User has no programs | (stays `loggedIn`) | AuthGuard/ProgramRedirect renders `<AuthErrorPage>` inline |
|
|
175
|
+
| Program fetch fails | (stays `loggedIn`) | AuthGuard renders `<AuthErrorPage>` inline |
|
|
176
|
+
| No programs (from LoginPage) | (stays `loggedIn`) | LoginPage redirects to `/auth-error?type=errorNoProgram` |
|
|
177
|
+
| Malicious `redirectUrl` | (stays `loggedIn`) | LoginPage discards URL, falls back to program or error page |
|
|
178
|
+
|
|
179
|
+
## Route Structure
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
/ → redirects to /login
|
|
183
|
+
/login → handles auth flow, error redirects, and post-auth navigation
|
|
184
|
+
/auth-error?type=... → displays auth error messages (errorInit, errorLoadUser, errorTokenRefresh, errorNoProgram)
|
|
185
|
+
/unauthorized → shown for users with 'unauthorized' role
|
|
186
|
+
/_auth-routes/ → protected layout (requires loginState === 'loggedIn')
|
|
187
|
+
│ - AuthGuard: redirects to /login if loggedOut
|
|
188
|
+
│ - renders null until loggedIn
|
|
189
|
+
│ - loads user programs, selects program, sets theme
|
|
190
|
+
│ - renders <AuthErrorPage> inline if no programs
|
|
191
|
+
│
|
|
192
|
+
├── $programId/ → DashboardWrapperPage (layout wrapper)
|
|
193
|
+
│ ├── index → redirects to /$programId/$deliverableId/workstream
|
|
194
|
+
│ │ (gets deliverableId from viewerData.programs[programId])
|
|
195
|
+
│ │ or renders <AuthErrorPage> inline if no programs
|
|
196
|
+
│ │
|
|
197
|
+
│ └── $deliverableId/
|
|
198
|
+
│ ├── index → redirects to /$programId/$deliverableId/workstream
|
|
199
|
+
│ │ (defaults to 'workstream' tab)
|
|
200
|
+
│ │
|
|
201
|
+
│ └── $tab → workstream, pnl, one-off-costs, etc.
|
|
202
|
+
│
|
|
203
|
+
└── admin/ → admin pages
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Redirect URL Preservation
|
|
207
|
+
|
|
208
|
+
When an unauthenticated user tries to access a protected route:
|
|
209
|
+
|
|
210
|
+
1. AuthGuard detects `loginState === 'loggedOut'` and navigates to `/login?redirectUrl=...`
|
|
211
|
+
2. The current URL (path + search params) is stored as the `redirectUrl` search parameter
|
|
212
|
+
3. After successful authentication, LoginPage sanitizes the `redirectUrl` (must be a relative path starting with `/`, rejecting protocol-relative `//` and absolute URLs) and redirects to the saved URL
|
|
213
|
+
4. Fallback: If no `redirectUrl` (or if it was rejected as unsafe), redirects to `/$programId` (using programId from localStorage or first available program), which then cascades through index routes to the final view
|
|
214
|
+
5. If user has no programs at all, redirects to `/auth-error?type=errorNoProgram`
|