@rashidee/co2 1.2.5

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.
Files changed (190) hide show
  1. package/dist/index.js +25449 -0
  2. package/drizzle/0000_remarkable_charles_xavier.sql +210 -0
  3. package/drizzle/0001_wise_wolf_cub.sql +13 -0
  4. package/drizzle/0002_secret_spyke.sql +16 -0
  5. package/drizzle/0003_workspace_config_singleton.sql +16 -0
  6. package/drizzle/0004_terminal_claim_v120.sql +11 -0
  7. package/drizzle/0005_brainstorm_v124.sql +10 -0
  8. package/drizzle/0006_project_v140.sql +6 -0
  9. package/drizzle/0007_prd_wizard_v124.sql +14 -0
  10. package/drizzle/0008_workflow_artifact_v126.sql +24 -0
  11. package/drizzle/0009_project_import_v150.sql +7 -0
  12. package/drizzle/0010_quality_v151.sql +3 -0
  13. package/drizzle/meta/0000_snapshot.json +1335 -0
  14. package/drizzle/meta/0001_snapshot.json +1416 -0
  15. package/drizzle/meta/0002_snapshot.json +1510 -0
  16. package/drizzle/meta/_journal.json +83 -0
  17. package/package.json +41 -0
  18. package/plugin/.claude-plugin/marketplace.json +36 -0
  19. package/plugin/.claude-plugin/plugin.json +22 -0
  20. package/plugin/README.md +625 -0
  21. package/plugin/SKILLS.md +350 -0
  22. package/plugin/co2.svg +5 -0
  23. package/plugin/commands/brainstorm-loop.md +129 -0
  24. package/plugin/skills/conductor-defect/SKILL.md +898 -0
  25. package/plugin/skills/conductor-feature-develop/SKILL.md +1383 -0
  26. package/plugin/skills/conductor-feature-develop/references/implementation-domain-template.md +131 -0
  27. package/plugin/skills/conductor-feature-develop/references/implementation-master-template.md +89 -0
  28. package/plugin/skills/conductor-feature-develop/references/playwright-setup.md +224 -0
  29. package/plugin/skills/conductor-feature-prepare/SKILL.md +604 -0
  30. package/plugin/skills/depgen-k8s/SKILL.md +468 -0
  31. package/plugin/skills/depgen-k8s/references/dockerfile-laravel.md +197 -0
  32. package/plugin/skills/depgen-k8s/references/dockerfile-nodejs.md +163 -0
  33. package/plugin/skills/depgen-k8s/references/dockerfile-spring-boot.md +174 -0
  34. package/plugin/skills/depgen-k8s/references/k8s-patterns.md +869 -0
  35. package/plugin/skills/mockgen-shadcn/SKILL.md +1067 -0
  36. package/plugin/skills/mockgen-shadcn/references/admin-layout-template.md +1467 -0
  37. package/plugin/skills/mockgen-shadcn/references/mockup-hub-template.md +498 -0
  38. package/plugin/skills/mockgen-shadcn/references/mockup-index-template.md +216 -0
  39. package/plugin/skills/mockgen-tailwind/SKILL.md +904 -0
  40. package/plugin/skills/mockgen-tailwind/references/admin-layout-template.md +720 -0
  41. package/plugin/skills/mockgen-tailwind/references/mockup-hub-template.md +498 -0
  42. package/plugin/skills/mockgen-tailwind/references/mockup-index-template.md +190 -0
  43. package/plugin/skills/modelgen-nosql/SKILL.md +617 -0
  44. package/plugin/skills/modelgen-nosql/references/model-extraction-methodology.md +497 -0
  45. package/plugin/skills/modelgen-nosql/references/nosql-design-guide.md +258 -0
  46. package/plugin/skills/modelgen-relational/SKILL.md +707 -0
  47. package/plugin/skills/modelgen-relational/references/model-extraction-methodology.md +499 -0
  48. package/plugin/skills/specgen-flutter-riverpod/SKILL.md +898 -0
  49. package/plugin/skills/specgen-flutter-riverpod/references/component-patterns.md +971 -0
  50. package/plugin/skills/specgen-flutter-riverpod/references/notification-patterns.md +457 -0
  51. package/plugin/skills/specgen-flutter-riverpod/references/routing-patterns.md +362 -0
  52. package/plugin/skills/specgen-flutter-riverpod/references/security-patterns.md +709 -0
  53. package/plugin/skills/specgen-flutter-riverpod/references/spec-template.md +1566 -0
  54. package/plugin/skills/specgen-flutter-riverpod/references/state-patterns.md +518 -0
  55. package/plugin/skills/specgen-flutter-riverpod/references/storage-patterns.md +370 -0
  56. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/SKILL.md +984 -0
  57. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/batch-patterns.md +359 -0
  58. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/messaging-patterns.md +468 -0
  59. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/modulith-patterns.md +461 -0
  60. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/reporting-patterns.md +803 -0
  61. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/security-patterns.md +501 -0
  62. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/spec-template.md +1268 -0
  63. package/plugin/skills/specgen-laravel-eloquent-bladehtmx/references/web-patterns.md +301 -0
  64. package/plugin/skills/specgen-node-cli-web/SKILL.md +561 -0
  65. package/plugin/skills/specgen-node-cli-web/references/auth-patterns.md +293 -0
  66. package/plugin/skills/specgen-node-cli-web/references/cli-packaging-patterns.md +250 -0
  67. package/plugin/skills/specgen-node-cli-web/references/server-patterns.md +247 -0
  68. package/plugin/skills/specgen-node-cli-web/references/spec-template.md +401 -0
  69. package/plugin/skills/specgen-node-cli-web/references/testing-patterns.md +174 -0
  70. package/plugin/skills/specgen-react-mui/SKILL.md +789 -0
  71. package/plugin/skills/specgen-react-mui/references/component-patterns.md +1077 -0
  72. package/plugin/skills/specgen-react-mui/references/reporting-patterns.md +1998 -0
  73. package/plugin/skills/specgen-react-mui/references/routing-patterns.md +461 -0
  74. package/plugin/skills/specgen-react-mui/references/security-patterns.md +599 -0
  75. package/plugin/skills/specgen-react-mui/references/spec-template.md +1030 -0
  76. package/plugin/skills/specgen-react-mui/references/state-patterns.md +327 -0
  77. package/plugin/skills/specgen-react-tailwind/SKILL.md +859 -0
  78. package/plugin/skills/specgen-react-tailwind/references/component-patterns.md +1212 -0
  79. package/plugin/skills/specgen-react-tailwind/references/reporting-patterns.md +1529 -0
  80. package/plugin/skills/specgen-react-tailwind/references/routing-patterns.md +669 -0
  81. package/plugin/skills/specgen-react-tailwind/references/security-patterns.md +656 -0
  82. package/plugin/skills/specgen-react-tailwind/references/spec-template.md +1237 -0
  83. package/plugin/skills/specgen-react-tailwind/references/state-patterns.md +327 -0
  84. package/plugin/skills/specgen-sdk-java/SKILL.md +933 -0
  85. package/plugin/skills/specgen-sdk-java/references/api-extraction-patterns.md +247 -0
  86. package/plugin/skills/specgen-sdk-java/references/http-client-patterns.md +495 -0
  87. package/plugin/skills/specgen-sdk-java/references/packaging-patterns.md +366 -0
  88. package/plugin/skills/specgen-sdk-java/references/spec-template.md +956 -0
  89. package/plugin/skills/specgen-spring-jpa-jtehtmx/SKILL.md +1284 -0
  90. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/batch-patterns.md +1224 -0
  91. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/jasper-patterns.md +1638 -0
  92. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/messaging-patterns.md +512 -0
  93. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/modulith-patterns.md +354 -0
  94. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/security-patterns.md +551 -0
  95. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/spec-template.md +2502 -0
  96. package/plugin/skills/specgen-spring-jpa-jtehtmx/references/web-patterns.md +503 -0
  97. package/plugin/skills/specgen-spring-jpa-restapi/SKILL.md +990 -0
  98. package/plugin/skills/specgen-spring-jpa-restapi/references/batch-patterns.md +1224 -0
  99. package/plugin/skills/specgen-spring-jpa-restapi/references/messaging-patterns.md +512 -0
  100. package/plugin/skills/specgen-spring-jpa-restapi/references/modulith-patterns.md +279 -0
  101. package/plugin/skills/specgen-spring-jpa-restapi/references/reporting-patterns.md +1536 -0
  102. package/plugin/skills/specgen-spring-jpa-restapi/references/restapi-patterns.md +722 -0
  103. package/plugin/skills/specgen-spring-jpa-restapi/references/security-patterns.md +351 -0
  104. package/plugin/skills/specgen-spring-jpa-restapi/references/spec-template.md +553 -0
  105. package/plugin/skills/specgen-ts-cli/SKILL.md +861 -0
  106. package/plugin/skills/specgen-ts-cli/references/async-patterns.md +1101 -0
  107. package/plugin/skills/specgen-ts-cli/references/command-patterns.md +383 -0
  108. package/plugin/skills/specgen-ts-cli/references/config-patterns.md +430 -0
  109. package/plugin/skills/specgen-ts-cli/references/database-patterns.md +750 -0
  110. package/plugin/skills/specgen-ts-cli/references/packaging-patterns.md +426 -0
  111. package/plugin/skills/specgen-ts-cli/references/spec-template.md +1391 -0
  112. package/plugin/skills/specgen-ts-cli/references/testing-patterns.md +909 -0
  113. package/plugin/skills/testgen-functional/SKILL.md +1381 -0
  114. package/plugin/skills/tracegen-matrix/SKILL.md +243 -0
  115. package/plugin/skills/util-preparek8senv/SKILL.md +422 -0
  116. package/plugin/skills/util-projectinit/SKILL.md +307 -0
  117. package/plugin/skills/util-projectinit/reference/brainstorm-loop.md +129 -0
  118. package/plugin/skills/util-projectinit/reference/brainstorm-protocol.md +144 -0
  119. package/plugin/skills/util-projectinit/reference/brainstorm-setup-guide.md +153 -0
  120. package/plugin/skills/util-projectinit/reference/brainstorm-template-appdev.md +87 -0
  121. package/plugin/skills/util-projectinit/reference/co2-context-generation-guide.md +761 -0
  122. package/plugin/skills/util-projectsync/SKILL.md +501 -0
  123. package/plugin/skills/util-updprd/SKILL.md +324 -0
  124. package/plugin/skills/util-usanalyzer/SKILL.md +307 -0
  125. package/plugin/skills/util-ustagger/SKILL.md +154 -0
  126. package/static/assets/abnfDiagram-VRR7QNED-DvPuwyCV.js +1 -0
  127. package/static/assets/arc-DRKw7sjs.js +1 -0
  128. package/static/assets/architectureDiagram-ZJ3FMSHR-CaNcxUbC.js +36 -0
  129. package/static/assets/blockDiagram-677ZJIJ3-Demd4AOK.js +132 -0
  130. package/static/assets/c4Diagram-LMCZKHZV-PVKUfc2x.js +10 -0
  131. package/static/assets/channel-C2nVbuwv.js +1 -0
  132. package/static/assets/chunk-2Q5K7J3B-CExf8ww_.js +1 -0
  133. package/static/assets/chunk-32BRIVSS-CFPaoONF.js +1 -0
  134. package/static/assets/chunk-5VM5RSS4-DE2esM67.js +15 -0
  135. package/static/assets/chunk-EX3LRPZG-SON_YdDG.js +231 -0
  136. package/static/assets/chunk-JWPE2WC7-Qxah8LNr.js +1 -0
  137. package/static/assets/chunk-MOJQB5TN-BcWGQV3j.js +88 -0
  138. package/static/assets/chunk-RYQCIY6F-DkDj-OR5.js +1 -0
  139. package/static/assets/chunk-V7JOEXUC-BlI1Mrah.js +206 -0
  140. package/static/assets/chunk-VR4S4FIN-Agf__duj.js +1 -0
  141. package/static/assets/chunk-XXDRQBXY-B5CfAh85.js +1 -0
  142. package/static/assets/classDiagram-OUVF2IWQ-DXJIljsR.js +1 -0
  143. package/static/assets/classDiagram-v2-EOCWNBFH-DXJIljsR.js +1 -0
  144. package/static/assets/cose-bilkent-JH36ORCC-DO0cQkcy.js +1 -0
  145. package/static/assets/cynefin-VYW2F7L2-CfOnsnXc.js +178 -0
  146. package/static/assets/cynefinDiagram-TSTJHNR4-BJ8tULAv.js +62 -0
  147. package/static/assets/cytoscape.esm-DTSO7Bv0.js +331 -0
  148. package/static/assets/dagre-VKFMJZFB-DfjwzHqP.js +4 -0
  149. package/static/assets/defaultLocale-DX6XiGOO.js +1 -0
  150. package/static/assets/diagram-FQU43EPY-CndSOIIM.js +3 -0
  151. package/static/assets/diagram-G47NLZAW-Czf5hN0Z.js +24 -0
  152. package/static/assets/diagram-NH7WQ7WH-COAyojGh.js +24 -0
  153. package/static/assets/diagram-OA4YK3LP-H8PoiN2m.js +30 -0
  154. package/static/assets/diagram-WEI45ONY-DzcaVzjp.js +41 -0
  155. package/static/assets/ebnfDiagram-CCIWWBDH-DPazGYLJ.js +1 -0
  156. package/static/assets/erDiagram-Q63AITRT-Bo-fubgF.js +85 -0
  157. package/static/assets/flowDiagram-23GEKE2U-CSvxUDwh.js +156 -0
  158. package/static/assets/ganttDiagram-NO4QXBWP-DnBveWX6.js +292 -0
  159. package/static/assets/gitGraphDiagram-IHSO6WYX-B3rVL8c-.js +106 -0
  160. package/static/assets/graph-C9eacEi8.js +1 -0
  161. package/static/assets/index-LZSQkLE2.css +41 -0
  162. package/static/assets/index-y7XDSgMG.js +495 -0
  163. package/static/assets/infoDiagram-FWYZ7A6U-CvWV9yy2.js +2 -0
  164. package/static/assets/init-Gi6I4Gst.js +1 -0
  165. package/static/assets/ishikawaDiagram-FXEZZL3T-DF28a1N6.js +70 -0
  166. package/static/assets/journeyDiagram-5HDEW3XC-CB6CugYk.js +139 -0
  167. package/static/assets/kanban-definition-HUTT4EX6-ResSLF6d.js +89 -0
  168. package/static/assets/katex-C5jXJg4s.js +257 -0
  169. package/static/assets/layout-DEXfKzaS.js +1 -0
  170. package/static/assets/linear-CLO--ryk.js +1 -0
  171. package/static/assets/map-Czzmt4hB.js +1 -0
  172. package/static/assets/mindmap-definition-LN4V7U3C-BvW3EbxX.js +96 -0
  173. package/static/assets/ordinal-Cboi1Yqb.js +1 -0
  174. package/static/assets/pegDiagram-2B236MQR-BHCgqrrM.js +1 -0
  175. package/static/assets/pieDiagram-ENE6RG2P-DVmCQ5FA.js +39 -0
  176. package/static/assets/quadrantDiagram-ABIIQ3AL-B3e5KR52.js +7 -0
  177. package/static/assets/railroadDiagram-RFXS5EU6-Csv5ysLc.js +1 -0
  178. package/static/assets/requirementDiagram-TGXJPOKE-DOMVzvnB.js +84 -0
  179. package/static/assets/sankeyDiagram-HTMAVEWB-B1dcTOvy.js +40 -0
  180. package/static/assets/sequenceDiagram-DBY2YBRQ-BY414yeO.js +162 -0
  181. package/static/assets/sizeCapture-X5ZJPWSS-CuJfrfBN.js +1 -0
  182. package/static/assets/stateDiagram-2N3HPSRC-FX4rJkzV.js +1 -0
  183. package/static/assets/stateDiagram-v2-6OUMAXLB-BVufbO_H.js +1 -0
  184. package/static/assets/swimlanes-5IMT3BWC-Sfy58mmv.js +2 -0
  185. package/static/assets/swimlanesDiagram-G3AALYLV-wDCDw08H.js +8 -0
  186. package/static/assets/timeline-definition-FHXFAJF6-CmV7by8p.js +120 -0
  187. package/static/assets/vennDiagram-L72KCM5P-FqIyFZFt.js +34 -0
  188. package/static/assets/wardleyDiagram-EHGQE667-BZ06O45i.js +78 -0
  189. package/static/assets/xychartDiagram-FW5EYKEG-BOkffR06.js +7 -0
  190. package/static/index.html +24 -0
@@ -0,0 +1,599 @@
1
+ # Security Patterns — OAuth2 PKCE (Keycloak) and Local JWT Auth
2
+
3
+ This reference describes the complete authentication architecture for the spec. Include
4
+ all of this content in Section 6 of the generated specification.
5
+
6
+ ---
7
+
8
+ ## Architecture Overview — Keycloak PKCE (Auth = Keycloak)
9
+
10
+ The application operates as an OAuth2 Public Client using Authorization Code flow with
11
+ PKCE (Proof Key for Code Exchange) and OIDC. Keycloak is the external identity provider
12
+ that handles login UI, user registration, and role management. The application's
13
+ responsibilities are:
14
+
15
+ 1. Redirect unauthenticated users to Keycloak login page (PKCE challenge generated client-side)
16
+ 2. Handle the OAuth2 callback and exchange the authorization code for tokens
17
+ 3. Store tokens securely in memory (Zustand store) — NOT in localStorage
18
+ 4. Extract roles from OIDC ID token claims
19
+ 5. Attach Bearer token to all API requests via Axios interceptor
20
+ 6. Silently refresh the access token before expiry
21
+
22
+ The flow:
23
+ ```
24
+ User navigates to protected route
25
+ → ProtectedRoute detects no auth
26
+ → oidc-client-ts generates code_verifier + code_challenge (PKCE)
27
+ → 302 Redirect to Keycloak /auth?response_type=code&code_challenge=...
28
+ → User logs in at Keycloak
29
+ → Keycloak redirects to /auth/callback?code=...
30
+ → AuthCallback page exchanges code for tokens (ID token + access token)
31
+ → react-oidc-context stores tokens in memory
32
+ → AuthProvider exposes user + token via useAuth() hook
33
+ → Axios interceptor reads token from auth context
34
+ → User redirected to originally-requested page
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Package Dependencies
40
+
41
+ ```json
42
+ {
43
+ "dependencies": {
44
+ "oidc-client-ts": "^3.1.0",
45
+ "react-oidc-context": "^3.2.0"
46
+ }
47
+ }
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Environment Variables
53
+
54
+ ```env
55
+ # .env.development
56
+ VITE_KEYCLOAK_AUTHORITY=http://localhost:8180/realms/{{KEYCLOAK_REALM}}
57
+ VITE_KEYCLOAK_CLIENT_ID={{KEYCLOAK_CLIENT_ID}}
58
+ VITE_KEYCLOAK_REDIRECT_URI=http://localhost:3000/auth/callback
59
+ VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URI=http://localhost:3000
60
+ VITE_KEYCLOAK_SCOPE=openid profile email
61
+
62
+ # .env.production
63
+ VITE_KEYCLOAK_AUTHORITY=${KEYCLOAK_AUTHORITY}
64
+ VITE_KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID}
65
+ VITE_KEYCLOAK_REDIRECT_URI=${APP_URL}/auth/callback
66
+ VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URI=${APP_URL}
67
+ VITE_KEYCLOAK_SCOPE=openid profile email
68
+ ```
69
+
70
+ ---
71
+
72
+ ## OIDC Configuration
73
+
74
+ ```typescript
75
+ // src/lib/auth/oidcConfig.ts
76
+ import type { AuthProviderProps } from 'react-oidc-context';
77
+
78
+ export const oidcConfig: AuthProviderProps = {
79
+ authority: import.meta.env.VITE_KEYCLOAK_AUTHORITY,
80
+ client_id: import.meta.env.VITE_KEYCLOAK_CLIENT_ID,
81
+ redirect_uri: import.meta.env.VITE_KEYCLOAK_REDIRECT_URI,
82
+ post_logout_redirect_uri: import.meta.env.VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URI,
83
+ scope: import.meta.env.VITE_KEYCLOAK_SCOPE,
84
+ response_type: 'code',
85
+ // PKCE is enabled by default in oidc-client-ts
86
+ // Silently renew the access token 60 seconds before expiry
87
+ automaticSilentRenew: true,
88
+ // Load user info from the OIDC userinfo endpoint
89
+ loadUserInfo: true,
90
+ onSigninCallback: (_user) => {
91
+ // Remove the auth params from the URL after successful login
92
+ window.history.replaceState({}, document.title, window.location.pathname);
93
+ },
94
+ };
95
+ ```
96
+
97
+ ---
98
+
99
+ ## AuthProvider Setup
100
+
101
+ ```tsx
102
+ // src/main.tsx
103
+ import React from 'react';
104
+ import ReactDOM from 'react-dom/client';
105
+ import { AuthProvider } from 'react-oidc-context';
106
+ import { QueryClientProvider } from '@tanstack/react-query';
107
+ import { oidcConfig } from './lib/auth/oidcConfig';
108
+ import { queryClient } from './lib/queryClient';
109
+ import { App } from './App';
110
+
111
+ ReactDOM.createRoot(document.getElementById('root')!).render(
112
+ <React.StrictMode>
113
+ <AuthProvider {...oidcConfig}>
114
+ <QueryClientProvider client={queryClient}>
115
+ <App />
116
+ </QueryClientProvider>
117
+ </AuthProvider>
118
+ </React.StrictMode>,
119
+ );
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Role Extraction from OIDC Token
125
+
126
+ Keycloak places roles in both `realm_access.roles` and `resource_access.<client_id>.roles`
127
+ within the ID token claims. Extract them using a utility:
128
+
129
+ ```typescript
130
+ // src/lib/auth/keycloakRoles.ts
131
+ import type { User } from 'oidc-client-ts';
132
+
133
+ /**
134
+ * Extract Keycloak roles from the OIDC user's ID token claims.
135
+ * Combines realm roles and client-specific resource roles.
136
+ */
137
+ export function extractKeycloakRoles(user: User | null | undefined): string[] {
138
+ if (!user?.profile) return [];
139
+
140
+ const profile = user.profile as Record<string, unknown>;
141
+ const roles: string[] = [];
142
+
143
+ // Realm-level roles
144
+ const realmAccess = profile['realm_access'] as { roles?: string[] } | undefined;
145
+ if (realmAccess?.roles) {
146
+ roles.push(...realmAccess.roles);
147
+ }
148
+
149
+ // Client-specific roles
150
+ const clientId = import.meta.env.VITE_KEYCLOAK_CLIENT_ID as string;
151
+ const resourceAccess = profile['resource_access'] as
152
+ Record<string, { roles?: string[] }> | undefined;
153
+ if (resourceAccess?.[clientId]?.roles) {
154
+ roles.push(...resourceAccess[clientId].roles);
155
+ }
156
+
157
+ return [...new Set(roles.map((r) => r.toUpperCase()))];
158
+ }
159
+
160
+ /**
161
+ * Check if the user has a specific role.
162
+ */
163
+ export function hasRole(user: User | null | undefined, role: string): boolean {
164
+ return extractKeycloakRoles(user).includes(role.toUpperCase());
165
+ }
166
+
167
+ /**
168
+ * Check if the user has any of the specified roles.
169
+ */
170
+ export function hasAnyRole(user: User | null | undefined, roles: string[]): boolean {
171
+ const userRoles = extractKeycloakRoles(user);
172
+ return roles.some((r) => userRoles.includes(r.toUpperCase()));
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## useAuthUser Hook
179
+
180
+ A thin wrapper over `react-oidc-context`'s `useAuth()` that adds role-based utilities:
181
+
182
+ ```typescript
183
+ // src/lib/auth/useAuthUser.ts
184
+ import { useAuth } from 'react-oidc-context';
185
+ import { extractKeycloakRoles, hasRole, hasAnyRole } from './keycloakRoles';
186
+
187
+ export interface AuthUser {
188
+ id: string;
189
+ username: string;
190
+ email: string;
191
+ name: string;
192
+ roles: string[];
193
+ accessToken: string | undefined;
194
+ isAuthenticated: boolean;
195
+ isLoading: boolean;
196
+ login: () => Promise<void>;
197
+ logout: () => Promise<void>;
198
+ hasRole: (role: string) => boolean;
199
+ hasAnyRole: (roles: string[]) => boolean;
200
+ }
201
+
202
+ export function useAuthUser(): AuthUser {
203
+ const auth = useAuth();
204
+ const roles = extractKeycloakRoles(auth.user);
205
+
206
+ return {
207
+ id: auth.user?.profile.sub ?? '',
208
+ username: (auth.user?.profile.preferred_username as string) ?? '',
209
+ email: auth.user?.profile.email ?? '',
210
+ name: auth.user?.profile.name ?? '',
211
+ roles,
212
+ accessToken: auth.user?.access_token,
213
+ isAuthenticated: auth.isAuthenticated,
214
+ isLoading: auth.isLoading,
215
+ login: () => auth.signinRedirect(),
216
+ logout: () =>
217
+ auth.signoutRedirect({
218
+ post_logout_redirect_uri: import.meta.env.VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URI,
219
+ }),
220
+ hasRole: (role) => hasRole(auth.user, role),
221
+ hasAnyRole: (r) => hasAnyRole(auth.user, r),
222
+ };
223
+ }
224
+ ```
225
+
226
+ ---
227
+
228
+ ## ProtectedRoute Component
229
+
230
+ ```tsx
231
+ // src/shared/components/ProtectedRoute.tsx
232
+ import { useAuth } from 'react-oidc-context';
233
+ import { Navigate, useLocation } from 'react-router-dom';
234
+ import { hasAnyRole } from '../../lib/auth/keycloakRoles';
235
+ import { LoadingOverlay } from './LoadingOverlay';
236
+
237
+ interface ProtectedRouteProps {
238
+ children: React.ReactNode;
239
+ /** If provided, the user must have at least one of these roles */
240
+ requiredRoles?: string[];
241
+ }
242
+
243
+ export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) {
244
+ const auth = useAuth();
245
+ const location = useLocation();
246
+
247
+ if (auth.isLoading) {
248
+ return <LoadingOverlay />;
249
+ }
250
+
251
+ if (!auth.isAuthenticated) {
252
+ // Trigger Keycloak login redirect
253
+ auth.signinRedirect({ state: { from: location.pathname } });
254
+ return <LoadingOverlay />;
255
+ }
256
+
257
+ if (requiredRoles && !hasAnyRole(auth.user, requiredRoles)) {
258
+ return <Navigate to="/unauthorized" replace />;
259
+ }
260
+
261
+ return <>{children}</>;
262
+ }
263
+ ```
264
+
265
+ ---
266
+
267
+ ## AuthCallback Page
268
+
269
+ ```tsx
270
+ // src/features/auth/pages/AuthCallbackPage.tsx
271
+ import { useEffect } from 'react';
272
+ import { useAuth } from 'react-oidc-context';
273
+ import { useNavigate } from 'react-router-dom';
274
+ import { LoadingOverlay } from '../../../shared/components/LoadingOverlay';
275
+
276
+ /**
277
+ * Handles the OAuth2 callback redirect from Keycloak.
278
+ * react-oidc-context processes the code automatically via onSigninCallback;
279
+ * this page just shows a loading state during the exchange.
280
+ */
281
+ export function AuthCallbackPage() {
282
+ const auth = useAuth();
283
+ const navigate = useNavigate();
284
+
285
+ useEffect(() => {
286
+ if (auth.isAuthenticated) {
287
+ // Navigate to the page the user was originally trying to access, or home
288
+ const from = (auth.user?.state as { from?: string })?.from ?? '/';
289
+ navigate(from, { replace: true });
290
+ }
291
+ }, [auth.isAuthenticated, auth.user, navigate]);
292
+
293
+ if (auth.error) {
294
+ return (
295
+ <div style={{ padding: 32 }}>
296
+ <h2>Authentication Error</h2>
297
+ <p>{auth.error.message}</p>
298
+ </div>
299
+ );
300
+ }
301
+
302
+ return <LoadingOverlay message="Completing sign-in..." />;
303
+ }
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Axios Interceptor (Keycloak)
309
+
310
+ ```typescript
311
+ // src/lib/api/axiosInstance.ts
312
+ import axios from 'axios';
313
+ import { getAuthUser } from './authBridge';
314
+
315
+ export const axiosInstance = axios.create({
316
+ baseURL: import.meta.env.VITE_API_BASE_URL,
317
+ timeout: 30_000,
318
+ headers: {
319
+ 'Content-Type': 'application/json',
320
+ },
321
+ });
322
+
323
+ // Attach Bearer token to every request
324
+ axiosInstance.interceptors.request.use(
325
+ (config) => {
326
+ const token = getAuthUser()?.access_token;
327
+ if (token) {
328
+ config.headers.Authorization = `Bearer ${token}`;
329
+ }
330
+ return config;
331
+ },
332
+ (error) => Promise.reject(error),
333
+ );
334
+
335
+ // Global error handling
336
+ axiosInstance.interceptors.response.use(
337
+ (response) => response,
338
+ (error) => {
339
+ if (axios.isAxiosError(error)) {
340
+ const status = error.response?.status;
341
+ if (status === 401) {
342
+ // Token expired — oidc-client-ts silent renewal handles this automatically.
343
+ // If silent renewal fails, the user is redirected to Keycloak login.
344
+ console.warn('Unauthorized — triggering silent renew');
345
+ } else if (status === 403) {
346
+ console.warn('Forbidden — insufficient permissions');
347
+ } else if (status && status >= 500) {
348
+ console.error('Server error', error.response?.data);
349
+ }
350
+ }
351
+ return Promise.reject(error);
352
+ },
353
+ );
354
+ ```
355
+
356
+ The `authBridge` module provides a way to access the current OIDC user from outside
357
+ React (needed for the Axios interceptor, which runs outside React's component tree):
358
+
359
+ ```typescript
360
+ // src/lib/api/authBridge.ts
361
+ import type { User } from 'oidc-client-ts';
362
+
363
+ let _currentUser: User | null = null;
364
+
365
+ export function setAuthUser(user: User | null): void {
366
+ _currentUser = user;
367
+ }
368
+
369
+ export function getAuthUser(): User | null {
370
+ return _currentUser;
371
+ }
372
+ ```
373
+
374
+ Register the bridge in the `App` component:
375
+
376
+ ```tsx
377
+ // src/App.tsx (relevant excerpt)
378
+ import { useAuth } from 'react-oidc-context';
379
+ import { useEffect } from 'react';
380
+ import { setAuthUser } from './lib/api/authBridge';
381
+
382
+ export function App() {
383
+ const auth = useAuth();
384
+
385
+ // Keep the auth bridge in sync with the current OIDC user
386
+ useEffect(() => {
387
+ setAuthUser(auth.user ?? null);
388
+ }, [auth.user]);
389
+
390
+ return (/* router and providers */);
391
+ }
392
+ ```
393
+
394
+ ---
395
+
396
+ ## OIDC ID Token Structure Reference
397
+
398
+ Keycloak places roles in the ID token as follows:
399
+
400
+ ```json
401
+ {
402
+ "sub": "f1234-abcd-5678",
403
+ "preferred_username": "john.doe",
404
+ "email": "john.doe@example.com",
405
+ "name": "John Doe",
406
+ "realm_access": {
407
+ "roles": ["USER", "offline_access"]
408
+ },
409
+ "resource_access": {
410
+ "{{KEYCLOAK_CLIENT_ID}}": {
411
+ "roles": ["ADMIN"]
412
+ }
413
+ },
414
+ "scope": "openid profile email"
415
+ }
416
+ ```
417
+
418
+ This structure is what `extractKeycloakRoles()` parses.
419
+
420
+ ---
421
+
422
+ ## Role Constants
423
+
424
+ Define roles as a constants object to avoid string duplication:
425
+
426
+ ```typescript
427
+ // src/lib/auth/roles.ts
428
+ export const Roles = {
429
+ ADMIN: 'ADMIN',
430
+ EDITOR: 'EDITOR',
431
+ USER: 'USER',
432
+ } as const;
433
+
434
+ export type Role = (typeof Roles)[keyof typeof Roles];
435
+ ```
436
+
437
+ ---
438
+
439
+ ## Architecture Overview — Local JWT Auth (Auth = Local)
440
+
441
+ When the backend manages authentication (no external IdP), the flow is:
442
+
443
+ ```
444
+ User submits login form
445
+ → POST /api/auth/login with { email, password }
446
+ → Backend returns { accessToken, user }
447
+ → Zustand auth store saves token in memory
448
+ → Axios interceptor reads token from Zustand store
449
+ → On page reload: token is lost, user must re-login (or use refresh token cookie)
450
+ ```
451
+
452
+ ### Zustand Auth Store (Local JWT)
453
+
454
+ ```typescript
455
+ // src/store/auth.store.ts
456
+ import { create } from 'zustand';
457
+ import { axiosInstance } from '../lib/api/axiosInstance';
458
+
459
+ interface UserInfo {
460
+ id: string;
461
+ email: string;
462
+ name: string;
463
+ roles: string[];
464
+ }
465
+
466
+ interface AuthState {
467
+ user: UserInfo | null;
468
+ accessToken: string | null;
469
+ isAuthenticated: boolean;
470
+ isLoading: boolean;
471
+ login: (email: string, password: string) => Promise<void>;
472
+ logout: () => Promise<void>;
473
+ setToken: (token: string, user: UserInfo) => void;
474
+ clearAuth: () => void;
475
+ }
476
+
477
+ export const useAuthStore = create<AuthState>((set) => ({
478
+ user: null,
479
+ accessToken: null,
480
+ isAuthenticated: false,
481
+ isLoading: false,
482
+
483
+ login: async (email, password) => {
484
+ set({ isLoading: true });
485
+ try {
486
+ const response = await axiosInstance.post<{ accessToken: string; user: UserInfo }>(
487
+ '/auth/login',
488
+ { email, password },
489
+ );
490
+ set({
491
+ accessToken: response.data.accessToken,
492
+ user: response.data.user,
493
+ isAuthenticated: true,
494
+ isLoading: false,
495
+ });
496
+ } catch (error) {
497
+ set({ isLoading: false });
498
+ throw error;
499
+ }
500
+ },
501
+
502
+ logout: async () => {
503
+ try {
504
+ await axiosInstance.post('/auth/logout');
505
+ } finally {
506
+ set({ user: null, accessToken: null, isAuthenticated: false });
507
+ }
508
+ },
509
+
510
+ setToken: (token, user) => set({ accessToken: token, user, isAuthenticated: true }),
511
+ clearAuth: () => set({ user: null, accessToken: null, isAuthenticated: false }),
512
+ }));
513
+ ```
514
+
515
+ ### Axios Interceptor (Local JWT)
516
+
517
+ ```typescript
518
+ // src/lib/api/axiosInstance.ts
519
+ import axios from 'axios';
520
+ import { useAuthStore } from '../../store/auth.store';
521
+
522
+ export const axiosInstance = axios.create({
523
+ baseURL: import.meta.env.VITE_API_BASE_URL,
524
+ timeout: 30_000,
525
+ withCredentials: true, // Required for httpOnly refresh token cookie
526
+ headers: { 'Content-Type': 'application/json' },
527
+ });
528
+
529
+ axiosInstance.interceptors.request.use((config) => {
530
+ // Read token directly from Zustand store state (outside React)
531
+ const token = useAuthStore.getState().accessToken;
532
+ if (token) {
533
+ config.headers.Authorization = `Bearer ${token}`;
534
+ }
535
+ return config;
536
+ });
537
+
538
+ axiosInstance.interceptors.response.use(
539
+ (response) => response,
540
+ async (error) => {
541
+ if (!axios.isAxiosError(error)) return Promise.reject(error);
542
+ const status = error.response?.status;
543
+ const originalRequest = error.config as typeof error.config & { _retry?: boolean };
544
+
545
+ if (status === 401 && !originalRequest._retry) {
546
+ originalRequest._retry = true;
547
+ try {
548
+ // Attempt token refresh using httpOnly cookie
549
+ const res = await axiosInstance.post<{ accessToken: string; user: unknown }>(
550
+ '/auth/refresh',
551
+ );
552
+ useAuthStore.getState().setToken(
553
+ res.data.accessToken,
554
+ res.data.user as Parameters<typeof useAuthStore.getState().setToken>[1],
555
+ );
556
+ // Retry the original request with the new token
557
+ originalRequest.headers = originalRequest.headers ?? {};
558
+ originalRequest.headers.Authorization = `Bearer ${res.data.accessToken}`;
559
+ return axiosInstance(originalRequest);
560
+ } catch {
561
+ useAuthStore.getState().clearAuth();
562
+ window.location.href = '/login';
563
+ }
564
+ }
565
+ return Promise.reject(error);
566
+ },
567
+ );
568
+ ```
569
+
570
+ ### ProtectedRoute Component (Local JWT)
571
+
572
+ ```tsx
573
+ // src/shared/components/ProtectedRoute.tsx
574
+ import { Navigate, useLocation } from 'react-router-dom';
575
+ import { useAuthStore } from '../../store/auth.store';
576
+ import { LoadingOverlay } from './LoadingOverlay';
577
+
578
+ interface ProtectedRouteProps {
579
+ children: React.ReactNode;
580
+ requiredRoles?: string[];
581
+ }
582
+
583
+ export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) {
584
+ const { isAuthenticated, isLoading, user } = useAuthStore();
585
+ const location = useLocation();
586
+
587
+ if (isLoading) return <LoadingOverlay />;
588
+
589
+ if (!isAuthenticated) {
590
+ return <Navigate to="/login" state={{ from: location.pathname }} replace />;
591
+ }
592
+
593
+ if (requiredRoles && !requiredRoles.some((r) => user?.roles.includes(r.toUpperCase()))) {
594
+ return <Navigate to="/unauthorized" replace />;
595
+ }
596
+
597
+ return <>{children}</>;
598
+ }
599
+ ```