@payez/next-mvp 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (654) hide show
  1. package/README.md +782 -0
  2. package/dist/api/auth-handler.d.ts +67 -0
  3. package/dist/api/auth-handler.js +397 -0
  4. package/dist/api/index.d.ts +10 -0
  5. package/dist/api/index.js +19 -0
  6. package/dist/api-handlers/account/change-password.d.ts +9 -0
  7. package/dist/api-handlers/account/change-password.js +112 -0
  8. package/dist/api-handlers/account/masked-info.d.ts +2 -0
  9. package/dist/api-handlers/account/masked-info.js +41 -0
  10. package/dist/api-handlers/account/profile.d.ts +3 -0
  11. package/dist/api-handlers/account/profile.js +63 -0
  12. package/dist/api-handlers/account/recovery/initiate.d.ts +2 -0
  13. package/dist/api-handlers/account/recovery/initiate.js +26 -0
  14. package/dist/api-handlers/account/recovery/send-code.d.ts +2 -0
  15. package/dist/api-handlers/account/recovery/send-code.js +28 -0
  16. package/dist/api-handlers/account/recovery/verify-code.d.ts +2 -0
  17. package/dist/api-handlers/account/recovery/verify-code.js +28 -0
  18. package/dist/api-handlers/account/reset-password.d.ts +2 -0
  19. package/dist/api-handlers/account/reset-password.js +26 -0
  20. package/dist/api-handlers/account/send-code.d.ts +24 -0
  21. package/dist/api-handlers/account/send-code.js +60 -0
  22. package/dist/api-handlers/account/update-phone.d.ts +27 -0
  23. package/dist/api-handlers/account/update-phone.js +64 -0
  24. package/dist/api-handlers/account/validate-password.d.ts +17 -0
  25. package/dist/api-handlers/account/validate-password.js +81 -0
  26. package/dist/api-handlers/account/verify-email.d.ts +26 -0
  27. package/dist/api-handlers/account/verify-email.js +106 -0
  28. package/dist/api-handlers/account/verify-sms.d.ts +26 -0
  29. package/dist/api-handlers/account/verify-sms.js +106 -0
  30. package/dist/api-handlers/admin/analytics.d.ts +20 -0
  31. package/dist/api-handlers/admin/analytics.js +379 -0
  32. package/dist/api-handlers/admin/audit.d.ts +20 -0
  33. package/dist/api-handlers/admin/audit.js +214 -0
  34. package/dist/api-handlers/admin/index.d.ts +21 -0
  35. package/dist/api-handlers/admin/index.js +41 -0
  36. package/dist/api-handlers/admin/redis-sessions.d.ts +36 -0
  37. package/dist/api-handlers/admin/redis-sessions.js +204 -0
  38. package/dist/api-handlers/admin/sessions.d.ts +21 -0
  39. package/dist/api-handlers/admin/sessions.js +284 -0
  40. package/dist/api-handlers/admin/site-logs.d.ts +46 -0
  41. package/dist/api-handlers/admin/site-logs.js +318 -0
  42. package/dist/api-handlers/admin/users.d.ts +20 -0
  43. package/dist/api-handlers/admin/users.js +222 -0
  44. package/dist/api-handlers/admin/vibe-data.d.ts +80 -0
  45. package/dist/api-handlers/admin/vibe-data.js +268 -0
  46. package/dist/api-handlers/anon/preferences.d.ts +37 -0
  47. package/dist/api-handlers/anon/preferences.js +96 -0
  48. package/dist/api-handlers/auth/jwks.d.ts +2 -0
  49. package/dist/api-handlers/auth/jwks.js +24 -0
  50. package/dist/api-handlers/auth/login.d.ts +42 -0
  51. package/dist/api-handlers/auth/login.js +178 -0
  52. package/dist/api-handlers/auth/refresh.d.ts +74 -0
  53. package/dist/api-handlers/auth/refresh.js +635 -0
  54. package/dist/api-handlers/auth/signout.d.ts +37 -0
  55. package/dist/api-handlers/auth/signout.js +187 -0
  56. package/dist/api-handlers/auth/status.d.ts +8 -0
  57. package/dist/api-handlers/auth/status.js +26 -0
  58. package/dist/api-handlers/auth/update-session.d.ts +37 -0
  59. package/dist/api-handlers/auth/update-session.js +95 -0
  60. package/dist/api-handlers/auth/validate.d.ts +6 -0
  61. package/dist/api-handlers/auth/validate.js +43 -0
  62. package/dist/api-handlers/auth/verify-code.d.ts +43 -0
  63. package/dist/api-handlers/auth/verify-code.js +94 -0
  64. package/dist/api-handlers/session/refresh-viability.d.ts +14 -0
  65. package/dist/api-handlers/session/refresh-viability.js +39 -0
  66. package/dist/api-handlers/session/viability.d.ts +13 -0
  67. package/dist/api-handlers/session/viability.js +146 -0
  68. package/dist/api-handlers/test/force-expire.d.ts +23 -0
  69. package/dist/api-handlers/test/force-expire.js +65 -0
  70. package/dist/auth/auth-decision.d.ts +39 -0
  71. package/dist/auth/auth-decision.js +182 -0
  72. package/dist/auth/auth-options.d.ts +57 -0
  73. package/dist/auth/auth-options.js +213 -0
  74. package/dist/auth/callbacks/index.d.ts +6 -0
  75. package/dist/auth/callbacks/index.js +12 -0
  76. package/dist/auth/callbacks/jwt.d.ts +45 -0
  77. package/dist/auth/callbacks/jwt.js +305 -0
  78. package/dist/auth/callbacks/session.d.ts +60 -0
  79. package/dist/auth/callbacks/session.js +170 -0
  80. package/dist/auth/callbacks/signin.d.ts +23 -0
  81. package/dist/auth/callbacks/signin.js +44 -0
  82. package/dist/auth/events/index.d.ts +4 -0
  83. package/dist/auth/events/index.js +8 -0
  84. package/dist/auth/events/signout.d.ts +17 -0
  85. package/dist/auth/events/signout.js +32 -0
  86. package/dist/auth/providers/credentials.d.ts +32 -0
  87. package/dist/auth/providers/credentials.js +223 -0
  88. package/dist/auth/providers/index.d.ts +5 -0
  89. package/dist/auth/providers/index.js +21 -0
  90. package/dist/auth/providers/oauth.d.ts +26 -0
  91. package/dist/auth/providers/oauth.js +105 -0
  92. package/dist/auth/route-config.d.ts +66 -0
  93. package/dist/auth/route-config.js +190 -0
  94. package/dist/auth/types/auth-types.d.ts +417 -0
  95. package/dist/auth/types/auth-types.js +53 -0
  96. package/dist/auth/types/index.d.ts +6 -0
  97. package/dist/auth/types/index.js +22 -0
  98. package/dist/auth/unauthenticated-routes.d.ts +1 -0
  99. package/dist/auth/unauthenticated-routes.js +19 -0
  100. package/dist/auth/utils/idp-client.d.ts +94 -0
  101. package/dist/auth/utils/idp-client.js +383 -0
  102. package/dist/auth/utils/index.d.ts +5 -0
  103. package/dist/auth/utils/index.js +21 -0
  104. package/dist/auth/utils/token-utils.d.ts +84 -0
  105. package/dist/auth/utils/token-utils.js +219 -0
  106. package/dist/client/AuthContext.d.ts +19 -0
  107. package/dist/client/AuthContext.js +112 -0
  108. package/dist/client/fetch-with-auth.d.ts +11 -0
  109. package/dist/client/fetch-with-auth.js +44 -0
  110. package/dist/client/fetchWithSession.d.ts +3 -0
  111. package/dist/client/fetchWithSession.js +24 -0
  112. package/dist/client/index.d.ts +9 -0
  113. package/dist/client/index.js +20 -0
  114. package/dist/client/useAnonSession.d.ts +36 -0
  115. package/dist/client/useAnonSession.js +99 -0
  116. package/dist/components/SessionSync.d.ts +13 -0
  117. package/dist/components/SessionSync.js +119 -0
  118. package/dist/components/SignalRHealthCheck.d.ts +10 -0
  119. package/dist/components/SignalRHealthCheck.js +97 -0
  120. package/dist/components/account/UserAvatarMenu.d.ts +20 -0
  121. package/dist/components/account/UserAvatarMenu.js +80 -0
  122. package/dist/components/account/index.d.ts +7 -0
  123. package/dist/components/account/index.js +10 -0
  124. package/dist/components/admin/AlertSettingsTab.d.ts +48 -0
  125. package/dist/components/admin/AlertSettingsTab.js +351 -0
  126. package/dist/components/admin/AnalyticsTab.d.ts +22 -0
  127. package/dist/components/admin/AnalyticsTab.js +167 -0
  128. package/dist/components/admin/DataBrowserTab.d.ts +19 -0
  129. package/dist/components/admin/DataBrowserTab.js +252 -0
  130. package/dist/components/admin/LoggingSettingsTab.d.ts +73 -0
  131. package/dist/components/admin/LoggingSettingsTab.js +339 -0
  132. package/dist/components/admin/SessionsTab.d.ts +37 -0
  133. package/dist/components/admin/SessionsTab.js +165 -0
  134. package/dist/components/admin/StatsTab.d.ts +53 -0
  135. package/dist/components/admin/StatsTab.js +161 -0
  136. package/dist/components/admin/VibeAdminContext.d.ts +32 -0
  137. package/dist/components/admin/VibeAdminContext.js +38 -0
  138. package/dist/components/admin/VibeAdminLayout.d.ts +11 -0
  139. package/dist/components/admin/VibeAdminLayout.js +69 -0
  140. package/dist/components/admin/index.d.ts +29 -0
  141. package/dist/components/admin/index.js +44 -0
  142. package/dist/components/auth/FederatedAuthSection.d.ts +8 -0
  143. package/dist/components/auth/FederatedAuthSection.js +45 -0
  144. package/dist/components/auth/ModeAwareLoginPage.d.ts +10 -0
  145. package/dist/components/auth/ModeAwareLoginPage.js +42 -0
  146. package/dist/components/auth/ModeAwareSignupPage.d.ts +9 -0
  147. package/dist/components/auth/ModeAwareSignupPage.js +78 -0
  148. package/dist/components/auth/TraditionalAuthSection.d.ts +14 -0
  149. package/dist/components/auth/TraditionalAuthSection.js +20 -0
  150. package/dist/components/recovery/CompleteStep.d.ts +5 -0
  151. package/dist/components/recovery/CompleteStep.js +8 -0
  152. package/dist/components/recovery/InitiateRecoveryStep.d.ts +8 -0
  153. package/dist/components/recovery/InitiateRecoveryStep.js +20 -0
  154. package/dist/components/recovery/SelectMethodStep.d.ts +8 -0
  155. package/dist/components/recovery/SelectMethodStep.js +8 -0
  156. package/dist/components/recovery/SetPasswordStep.d.ts +6 -0
  157. package/dist/components/recovery/SetPasswordStep.js +20 -0
  158. package/dist/components/recovery/VerifyCodeStep.d.ts +10 -0
  159. package/dist/components/recovery/VerifyCodeStep.js +24 -0
  160. package/dist/components/reserved/ReservedRecoveryWarning.d.ts +38 -0
  161. package/dist/components/reserved/ReservedRecoveryWarning.js +92 -0
  162. package/dist/components/reserved/ReservedStatusBox.d.ts +30 -0
  163. package/dist/components/reserved/ReservedStatusBox.js +71 -0
  164. package/dist/components/ui/BetaBadge.d.ts +29 -0
  165. package/dist/components/ui/BetaBadge.js +38 -0
  166. package/dist/components/ui/Footer.d.ts +37 -0
  167. package/dist/components/ui/Footer.js +41 -0
  168. package/dist/config/env.d.ts +66 -0
  169. package/dist/config/env.js +57 -0
  170. package/dist/config/logger.d.ts +57 -0
  171. package/dist/config/logger.js +73 -0
  172. package/dist/config/logging-config.d.ts +30 -0
  173. package/dist/config/logging-config.js +122 -0
  174. package/dist/config/unauthenticated-routes.d.ts +17 -0
  175. package/dist/config/unauthenticated-routes.js +24 -0
  176. package/dist/config/vibe-log-transport.d.ts +79 -0
  177. package/dist/config/vibe-log-transport.js +203 -0
  178. package/dist/edge/internal-api-url.d.ts +53 -0
  179. package/dist/edge/internal-api-url.js +63 -0
  180. package/dist/edge/middleware.d.ts +14 -0
  181. package/dist/edge/middleware.js +32 -0
  182. package/dist/hooks/useAuth.d.ts +23 -0
  183. package/dist/hooks/useAuth.js +81 -0
  184. package/dist/hooks/useAuthSettings.d.ts +59 -0
  185. package/dist/hooks/useAuthSettings.js +93 -0
  186. package/dist/hooks/useAvailableProviders.d.ts +45 -0
  187. package/dist/hooks/useAvailableProviders.js +108 -0
  188. package/dist/hooks/usePasswordValidation.d.ts +27 -0
  189. package/dist/hooks/usePasswordValidation.js +102 -0
  190. package/dist/hooks/useProfile.d.ts +15 -0
  191. package/dist/hooks/useProfile.js +59 -0
  192. package/dist/hooks/usePublicAuthSettings.d.ts +56 -0
  193. package/dist/hooks/usePublicAuthSettings.js +131 -0
  194. package/dist/hooks/useSessionExpiration.d.ts +57 -0
  195. package/dist/hooks/useSessionExpiration.js +72 -0
  196. package/dist/hooks/useViabilitySession.d.ts +75 -0
  197. package/dist/hooks/useViabilitySession.js +268 -0
  198. package/dist/index.d.ts +12 -0
  199. package/dist/index.js +54 -0
  200. package/dist/lib/anon-session.d.ts +74 -0
  201. package/dist/lib/anon-session.js +169 -0
  202. package/dist/lib/api-handler.d.ts +123 -0
  203. package/dist/lib/api-handler.js +478 -0
  204. package/dist/lib/app-slug.d.ts +95 -0
  205. package/dist/lib/app-slug.js +172 -0
  206. package/dist/lib/demo-mode.d.ts +6 -0
  207. package/dist/lib/demo-mode.js +16 -0
  208. package/dist/lib/geolocation.d.ts +64 -0
  209. package/dist/lib/geolocation.js +235 -0
  210. package/dist/lib/idp-client-config.d.ts +75 -0
  211. package/dist/lib/idp-client-config.js +351 -0
  212. package/dist/lib/idp-fetch.d.ts +14 -0
  213. package/dist/lib/idp-fetch.js +91 -0
  214. package/dist/lib/internal-api.d.ts +87 -0
  215. package/dist/lib/internal-api.js +122 -0
  216. package/dist/lib/jwt-decode-client.d.ts +10 -0
  217. package/dist/lib/jwt-decode-client.js +46 -0
  218. package/dist/lib/jwt-decode.d.ts +48 -0
  219. package/dist/lib/jwt-decode.js +57 -0
  220. package/dist/lib/nextauth-secret.d.ts +10 -0
  221. package/dist/lib/nextauth-secret.js +104 -0
  222. package/dist/lib/rate-limit-service.d.ts +23 -0
  223. package/dist/lib/rate-limit-service.js +6 -0
  224. package/dist/lib/redis.d.ts +5 -0
  225. package/dist/lib/redis.js +28 -0
  226. package/dist/lib/refresh-token-validator.d.ts +13 -0
  227. package/dist/lib/refresh-token-validator.js +117 -0
  228. package/dist/lib/roles.d.ts +145 -0
  229. package/dist/lib/roles.js +168 -0
  230. package/dist/lib/secret-validation.d.ts +4 -0
  231. package/dist/lib/secret-validation.js +14 -0
  232. package/dist/lib/session-store.d.ts +166 -0
  233. package/dist/lib/session-store.js +537 -0
  234. package/dist/lib/session.d.ts +21 -0
  235. package/dist/lib/session.js +26 -0
  236. package/dist/lib/site-logger.d.ts +214 -0
  237. package/dist/lib/site-logger.js +210 -0
  238. package/dist/lib/standardized-client-api.d.ts +161 -0
  239. package/dist/lib/standardized-client-api.js +786 -0
  240. package/dist/lib/startup-init.d.ts +40 -0
  241. package/dist/lib/startup-init.js +261 -0
  242. package/dist/lib/test-aware-get-token.d.ts +2 -0
  243. package/dist/lib/test-aware-get-token.js +81 -0
  244. package/dist/lib/token-expiry.d.ts +14 -0
  245. package/dist/lib/token-expiry.js +39 -0
  246. package/dist/lib/token-lifecycle.d.ts +52 -0
  247. package/dist/lib/token-lifecycle.js +398 -0
  248. package/dist/lib/types/api-responses.d.ts +128 -0
  249. package/dist/lib/types/api-responses.js +171 -0
  250. package/dist/lib/user-agent-parser.d.ts +50 -0
  251. package/dist/lib/user-agent-parser.js +220 -0
  252. package/dist/logging/api/admin-analytics.d.ts +3 -0
  253. package/dist/logging/api/admin-analytics.js +45 -0
  254. package/dist/logging/api/audit-log.d.ts +3 -0
  255. package/dist/logging/api/audit-log.js +52 -0
  256. package/dist/logging/components/AdminAnalyticsLayout.d.ts +10 -0
  257. package/dist/logging/components/AdminAnalyticsLayout.js +11 -0
  258. package/dist/logging/components/AuditLogViewer.d.ts +7 -0
  259. package/dist/logging/components/AuditLogViewer.js +51 -0
  260. package/dist/logging/components/ErrorMetricsCard.d.ts +7 -0
  261. package/dist/logging/components/ErrorMetricsCard.js +16 -0
  262. package/dist/logging/components/HealthMetricsCard.d.ts +7 -0
  263. package/dist/logging/components/HealthMetricsCard.js +19 -0
  264. package/dist/logging/hooks/useAdminAnalytics.d.ts +24 -0
  265. package/dist/logging/hooks/useAdminAnalytics.js +22 -0
  266. package/dist/logging/hooks/useAuditLog.d.ts +6 -0
  267. package/dist/logging/hooks/useAuditLog.js +25 -0
  268. package/dist/logging/hooks/useErrorMetrics.d.ts +6 -0
  269. package/dist/logging/hooks/useErrorMetrics.js +38 -0
  270. package/dist/logging/hooks/useHealthMetrics.d.ts +6 -0
  271. package/dist/logging/hooks/useHealthMetrics.js +41 -0
  272. package/dist/logging/index.d.ts +11 -0
  273. package/dist/logging/index.js +40 -0
  274. package/dist/logging/types/analytics.d.ts +68 -0
  275. package/dist/logging/types/analytics.js +3 -0
  276. package/dist/logging/types/audit.d.ts +29 -0
  277. package/dist/logging/types/audit.js +2 -0
  278. package/dist/logging/types/index.d.ts +2 -0
  279. package/dist/logging/types/index.js +19 -0
  280. package/dist/middleware/auth-decision.d.ts +33 -0
  281. package/dist/middleware/auth-decision.js +65 -0
  282. package/dist/middleware/create-middleware.d.ts +100 -0
  283. package/dist/middleware/create-middleware.js +445 -0
  284. package/dist/middleware/rbac-check.d.ts +44 -0
  285. package/dist/middleware/rbac-check.js +191 -0
  286. package/dist/middleware/twofa-presets.d.ts +134 -0
  287. package/dist/middleware/twofa-presets.js +175 -0
  288. package/dist/models/DecodedAccessToken.d.ts +17 -0
  289. package/dist/models/DecodedAccessToken.js +2 -0
  290. package/dist/models/SessionModel.d.ts +122 -0
  291. package/dist/models/SessionModel.js +136 -0
  292. package/dist/pages/admin-login/page.d.ts +31 -0
  293. package/dist/pages/admin-login/page.js +83 -0
  294. package/dist/pages/admin-roles/RolesAdminPage.d.ts +15 -0
  295. package/dist/pages/admin-roles/RolesAdminPage.js +78 -0
  296. package/dist/pages/admin-roles/index.d.ts +8 -0
  297. package/dist/pages/admin-roles/index.js +15 -0
  298. package/dist/pages/admin-roles/modals.d.ts +72 -0
  299. package/dist/pages/admin-roles/modals.js +154 -0
  300. package/dist/pages/client-admin/ClientSiteAdminPage.d.ts +79 -0
  301. package/dist/pages/client-admin/ClientSiteAdminPage.js +177 -0
  302. package/dist/pages/client-admin/index.d.ts +32 -0
  303. package/dist/pages/client-admin/index.js +37 -0
  304. package/dist/pages/login/page.d.ts +22 -0
  305. package/dist/pages/login/page.js +239 -0
  306. package/dist/pages/profile/EnhancedProfilePage.d.ts +13 -0
  307. package/dist/pages/profile/EnhancedProfilePage.js +150 -0
  308. package/dist/pages/profile/index.d.ts +8 -0
  309. package/dist/pages/profile/index.js +16 -0
  310. package/dist/pages/profile/page.d.ts +19 -0
  311. package/dist/pages/profile/page.js +47 -0
  312. package/dist/pages/profile/profile-patch.d.ts +1 -0
  313. package/dist/pages/profile/profile-patch.js +281 -0
  314. package/dist/pages/recovery/page.d.ts +1 -0
  315. package/dist/pages/recovery/page.js +142 -0
  316. package/dist/pages/roles/MyRolesPage.d.ts +24 -0
  317. package/dist/pages/roles/MyRolesPage.js +71 -0
  318. package/dist/pages/roles/components.d.ts +63 -0
  319. package/dist/pages/roles/components.js +108 -0
  320. package/dist/pages/roles/index.d.ts +8 -0
  321. package/dist/pages/roles/index.js +19 -0
  322. package/dist/pages/security/EnhancedSecurityPage.d.ts +14 -0
  323. package/dist/pages/security/EnhancedSecurityPage.js +248 -0
  324. package/dist/pages/security/index.d.ts +8 -0
  325. package/dist/pages/security/index.js +16 -0
  326. package/dist/pages/security/page.d.ts +21 -0
  327. package/dist/pages/security/page.js +212 -0
  328. package/dist/pages/security/security-patch.d.ts +1 -0
  329. package/dist/pages/security/security-patch.js +302 -0
  330. package/dist/pages/settings/EnhancedSettingsPage.d.ts +46 -0
  331. package/dist/pages/settings/EnhancedSettingsPage.js +231 -0
  332. package/dist/pages/settings/index.d.ts +8 -0
  333. package/dist/pages/settings/index.js +16 -0
  334. package/dist/pages/settings/page.d.ts +7 -0
  335. package/dist/pages/settings/page.js +26 -0
  336. package/dist/pages/showcase/ShowcasePage.d.ts +13 -0
  337. package/dist/pages/showcase/ShowcasePage.js +140 -0
  338. package/dist/pages/showcase/index.d.ts +12 -0
  339. package/dist/pages/showcase/index.js +17 -0
  340. package/dist/pages/test-env/EmergencyLogoutPage.d.ts +14 -0
  341. package/dist/pages/test-env/EmergencyLogoutPage.js +98 -0
  342. package/dist/pages/test-env/JwtInspectPage.d.ts +14 -0
  343. package/dist/pages/test-env/JwtInspectPage.js +114 -0
  344. package/dist/pages/test-env/RefreshTokenPage.d.ts +15 -0
  345. package/dist/pages/test-env/RefreshTokenPage.js +91 -0
  346. package/dist/pages/test-env/TestEnvPage.d.ts +13 -0
  347. package/dist/pages/test-env/TestEnvPage.js +49 -0
  348. package/dist/pages/test-env/index.d.ts +24 -0
  349. package/dist/pages/test-env/index.js +32 -0
  350. package/dist/pages/verify-code/page.d.ts +30 -0
  351. package/dist/pages/verify-code/page.js +408 -0
  352. package/dist/routes/account/index.d.ts +28 -0
  353. package/dist/routes/account/index.js +71 -0
  354. package/dist/routes/account/masked-info.d.ts +33 -0
  355. package/dist/routes/account/masked-info.js +39 -0
  356. package/dist/routes/account/send-code.d.ts +37 -0
  357. package/dist/routes/account/send-code.js +42 -0
  358. package/dist/routes/account/update-phone.d.ts +13 -0
  359. package/dist/routes/account/update-phone.js +17 -0
  360. package/dist/routes/account/verify-email.d.ts +38 -0
  361. package/dist/routes/account/verify-email.js +43 -0
  362. package/dist/routes/account/verify-sms.d.ts +38 -0
  363. package/dist/routes/account/verify-sms.js +43 -0
  364. package/dist/routes/auth/index.d.ts +19 -0
  365. package/dist/routes/auth/index.js +64 -0
  366. package/dist/routes/auth/logout.d.ts +31 -0
  367. package/dist/routes/auth/logout.js +113 -0
  368. package/dist/routes/auth/nextauth.d.ts +19 -0
  369. package/dist/routes/auth/nextauth.js +72 -0
  370. package/dist/routes/auth/refresh.d.ts +30 -0
  371. package/dist/routes/auth/refresh.js +51 -0
  372. package/dist/routes/auth/session.d.ts +72 -0
  373. package/dist/routes/auth/session.js +180 -0
  374. package/dist/routes/auth/settings.d.ts +25 -0
  375. package/dist/routes/auth/settings.js +55 -0
  376. package/dist/routes/auth/viability.d.ts +52 -0
  377. package/dist/routes/auth/viability.js +201 -0
  378. package/dist/routes/index.d.ts +12 -0
  379. package/dist/routes/index.js +54 -0
  380. package/dist/routes/session/index.d.ts +6 -0
  381. package/dist/routes/session/index.js +10 -0
  382. package/dist/routes/session/refresh-viability.d.ts +16 -0
  383. package/dist/routes/session/refresh-viability.js +20 -0
  384. package/dist/services/signalrActivityService.d.ts +44 -0
  385. package/dist/services/signalrActivityService.js +257 -0
  386. package/dist/stores/authStore.d.ts +154 -0
  387. package/dist/stores/authStore.js +1531 -0
  388. package/dist/theme/ThemeProvider.d.ts +14 -0
  389. package/dist/theme/ThemeProvider.js +28 -0
  390. package/dist/theme/default.d.ts +8 -0
  391. package/dist/theme/default.js +33 -0
  392. package/dist/theme/index.d.ts +15 -0
  393. package/dist/theme/index.js +25 -0
  394. package/dist/theme/types.d.ts +56 -0
  395. package/dist/theme/types.js +8 -0
  396. package/dist/theme/useTheme.d.ts +60 -0
  397. package/dist/theme/useTheme.js +63 -0
  398. package/dist/theme/utils.d.ts +13 -0
  399. package/dist/theme/utils.js +39 -0
  400. package/dist/types/api.d.ts +134 -0
  401. package/dist/types/api.js +44 -0
  402. package/dist/types/auth.d.ts +19 -0
  403. package/dist/types/auth.js +2 -0
  404. package/dist/types/logging.d.ts +42 -0
  405. package/dist/types/logging.js +2 -0
  406. package/dist/types/recovery.d.ts +48 -0
  407. package/dist/types/recovery.js +2 -0
  408. package/dist/types/security.d.ts +1 -0
  409. package/dist/types/security.js +2 -0
  410. package/dist/utils/api.d.ts +85 -0
  411. package/dist/utils/api.js +287 -0
  412. package/dist/utils/circuitBreaker.d.ts +43 -0
  413. package/dist/utils/circuitBreaker.js +91 -0
  414. package/dist/utils/error-message.d.ts +1 -0
  415. package/dist/utils/error-message.js +103 -0
  416. package/dist/utils/layout/reservedSpace.d.ts +59 -0
  417. package/dist/utils/layout/reservedSpace.js +102 -0
  418. package/dist/utils/logout.d.ts +14 -0
  419. package/dist/utils/logout.js +32 -0
  420. package/dist/vibe/client.d.ts +261 -0
  421. package/dist/vibe/client.js +445 -0
  422. package/dist/vibe/errors.d.ts +83 -0
  423. package/dist/vibe/errors.js +146 -0
  424. package/dist/vibe/generic.d.ts +234 -0
  425. package/dist/vibe/generic.js +369 -0
  426. package/dist/vibe/hooks/index.d.ts +169 -0
  427. package/dist/vibe/hooks/index.js +252 -0
  428. package/dist/vibe/index.d.ts +23 -0
  429. package/dist/vibe/index.js +67 -0
  430. package/dist/vibe/sessions.d.ts +161 -0
  431. package/dist/vibe/sessions.js +391 -0
  432. package/dist/vibe/types.d.ts +353 -0
  433. package/dist/vibe/types.js +315 -0
  434. package/package.json +855 -0
  435. package/scripts/check-internal-url-usage.sh +73 -0
  436. package/scripts/dev-broker.ps1 +35 -0
  437. package/scripts/dev-local.ps1 +45 -0
  438. package/src/api/auth-handler.ts +550 -0
  439. package/src/api/index.ts +18 -0
  440. package/src/api-handlers/account/change-password.ts +145 -0
  441. package/src/api-handlers/account/masked-info.ts +45 -0
  442. package/src/api-handlers/account/profile.ts +80 -0
  443. package/src/api-handlers/account/recovery/initiate.ts +23 -0
  444. package/src/api-handlers/account/recovery/send-code.ts +25 -0
  445. package/src/api-handlers/account/recovery/verify-code.ts +25 -0
  446. package/src/api-handlers/account/reset-password.ts +23 -0
  447. package/src/api-handlers/account/send-code.ts +76 -0
  448. package/src/api-handlers/account/update-phone.ts +79 -0
  449. package/src/api-handlers/account/validate-password.ts +118 -0
  450. package/src/api-handlers/account/verify-email.ts +125 -0
  451. package/src/api-handlers/account/verify-sms.ts +125 -0
  452. package/src/api-handlers/admin/analytics.ts +445 -0
  453. package/src/api-handlers/admin/audit.ts +225 -0
  454. package/src/api-handlers/admin/index.ts +59 -0
  455. package/src/api-handlers/admin/redis-sessions.ts +253 -0
  456. package/src/api-handlers/admin/sessions.ts +320 -0
  457. package/src/api-handlers/admin/site-logs.ts +367 -0
  458. package/src/api-handlers/admin/users.ts +244 -0
  459. package/src/api-handlers/admin/vibe-data.ts +326 -0
  460. package/src/api-handlers/anon/preferences.ts +123 -0
  461. package/src/api-handlers/auth/jwks.ts +20 -0
  462. package/src/api-handlers/auth/login.ts +240 -0
  463. package/src/api-handlers/auth/refresh.ts +687 -0
  464. package/src/api-handlers/auth/signout.ts +212 -0
  465. package/src/api-handlers/auth/status.ts +23 -0
  466. package/src/api-handlers/auth/update-session.ts +125 -0
  467. package/src/api-handlers/auth/validate.ts +44 -0
  468. package/src/api-handlers/auth/verify-code.ts +129 -0
  469. package/src/api-handlers/session/refresh-viability.ts +36 -0
  470. package/src/api-handlers/session/viability.ts +166 -0
  471. package/src/api-handlers/test/force-expire.ts +67 -0
  472. package/src/auth/auth-decision.ts +230 -0
  473. package/src/auth/auth-options.ts +237 -0
  474. package/src/auth/callbacks/index.ts +7 -0
  475. package/src/auth/callbacks/jwt.ts +382 -0
  476. package/src/auth/callbacks/session.ts +243 -0
  477. package/src/auth/callbacks/signin.ts +56 -0
  478. package/src/auth/events/index.ts +5 -0
  479. package/src/auth/events/signout.ts +33 -0
  480. package/src/auth/providers/credentials.ts +256 -0
  481. package/src/auth/providers/index.ts +6 -0
  482. package/src/auth/providers/oauth.ts +114 -0
  483. package/src/auth/route-config.ts +220 -0
  484. package/src/auth/types/auth-types.ts +555 -0
  485. package/src/auth/types/index.ts +7 -0
  486. package/src/auth/unauthenticated-routes.ts +3 -0
  487. package/src/auth/utils/idp-client.ts +444 -0
  488. package/src/auth/utils/index.ts +6 -0
  489. package/src/auth/utils/token-utils.ts +244 -0
  490. package/src/client/AuthContext.tsx +140 -0
  491. package/src/client/fetch-with-auth.ts +48 -0
  492. package/src/client/fetchWithSession.ts +21 -0
  493. package/src/client/index.ts +13 -0
  494. package/src/client/useAnonSession.ts +131 -0
  495. package/src/components/SessionSync.tsx +137 -0
  496. package/src/components/SignalRHealthCheck.tsx +131 -0
  497. package/src/components/account/UserAvatarMenu.tsx +217 -0
  498. package/src/components/account/index.ts +8 -0
  499. package/src/components/admin/AlertSettingsTab.tsx +728 -0
  500. package/src/components/admin/AnalyticsTab.tsx +703 -0
  501. package/src/components/admin/DataBrowserTab.tsx +505 -0
  502. package/src/components/admin/LoggingSettingsTab.tsx +665 -0
  503. package/src/components/admin/SessionsTab.tsx +414 -0
  504. package/src/components/admin/StatsTab.tsx +379 -0
  505. package/src/components/admin/VibeAdminContext.tsx +87 -0
  506. package/src/components/admin/VibeAdminLayout.tsx +185 -0
  507. package/src/components/admin/index.ts +59 -0
  508. package/src/components/auth/FederatedAuthSection.tsx +95 -0
  509. package/src/components/auth/ModeAwareLoginPage.tsx +135 -0
  510. package/src/components/auth/ModeAwareSignupPage.tsx +267 -0
  511. package/src/components/auth/TraditionalAuthSection.tsx +99 -0
  512. package/src/components/recovery/CompleteStep.tsx +36 -0
  513. package/src/components/recovery/InitiateRecoveryStep.tsx +68 -0
  514. package/src/components/recovery/SelectMethodStep.tsx +73 -0
  515. package/src/components/recovery/SetPasswordStep.tsx +97 -0
  516. package/src/components/recovery/VerifyCodeStep.tsx +90 -0
  517. package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -0
  518. package/src/components/reserved/ReservedStatusBox.tsx +118 -0
  519. package/src/components/ui/BetaBadge.tsx +58 -0
  520. package/src/components/ui/Footer.tsx +93 -0
  521. package/src/config/env.ts +57 -0
  522. package/src/config/logger.ts +62 -0
  523. package/src/config/logging-config.ts +82 -0
  524. package/src/config/unauthenticated-routes.ts +19 -0
  525. package/src/config/vibe-log-transport.ts +250 -0
  526. package/src/edge/internal-api-url.ts +65 -0
  527. package/src/edge/middleware.ts +42 -0
  528. package/src/hooks/useAuth.ts +115 -0
  529. package/src/hooks/useAuthSettings.ts +97 -0
  530. package/src/hooks/useAvailableProviders.ts +118 -0
  531. package/src/hooks/usePasswordValidation.ts +127 -0
  532. package/src/hooks/useProfile.ts +75 -0
  533. package/src/hooks/usePublicAuthSettings.ts +149 -0
  534. package/src/hooks/useSessionExpiration.ts +102 -0
  535. package/src/hooks/useViabilitySession.ts +335 -0
  536. package/src/index.ts +63 -0
  537. package/src/lib/anon-session.ts +213 -0
  538. package/src/lib/api-handler.ts +625 -0
  539. package/src/lib/app-slug.ts +178 -0
  540. package/src/lib/demo-mode.ts +13 -0
  541. package/src/lib/geolocation.ts +265 -0
  542. package/src/lib/idp-client-config.ts +442 -0
  543. package/src/lib/idp-fetch.ts +101 -0
  544. package/src/lib/internal-api.ts +171 -0
  545. package/src/lib/jwt-decode-client.ts +45 -0
  546. package/src/lib/jwt-decode.ts +83 -0
  547. package/src/lib/nextauth-secret.ts +126 -0
  548. package/src/lib/rate-limit-service.ts +9 -0
  549. package/src/lib/redis.ts +27 -0
  550. package/src/lib/refresh-token-validator.ts +64 -0
  551. package/src/lib/roles.ts +177 -0
  552. package/src/lib/secret-validation.ts +8 -0
  553. package/src/lib/session-store.ts +637 -0
  554. package/src/lib/session.ts +34 -0
  555. package/src/lib/site-logger.ts +245 -0
  556. package/src/lib/standardized-client-api.ts +896 -0
  557. package/src/lib/startup-init.ts +247 -0
  558. package/src/lib/test-aware-get-token.ts +30 -0
  559. package/src/lib/token-expiry.ts +40 -0
  560. package/src/lib/token-lifecycle.ts +477 -0
  561. package/src/lib/types/api-responses.ts +336 -0
  562. package/src/lib/user-agent-parser.ts +252 -0
  563. package/src/logging/api/admin-analytics.ts +51 -0
  564. package/src/logging/api/audit-log.ts +53 -0
  565. package/src/logging/components/AdminAnalyticsLayout.tsx +49 -0
  566. package/src/logging/components/AuditLogViewer.tsx +125 -0
  567. package/src/logging/components/ErrorMetricsCard.tsx +98 -0
  568. package/src/logging/components/HealthMetricsCard.tsx +70 -0
  569. package/src/logging/hooks/useAdminAnalytics.ts +22 -0
  570. package/src/logging/hooks/useAuditLog.ts +24 -0
  571. package/src/logging/hooks/useErrorMetrics.ts +40 -0
  572. package/src/logging/hooks/useHealthMetrics.ts +44 -0
  573. package/src/logging/index.ts +18 -0
  574. package/src/logging/types/analytics.ts +81 -0
  575. package/src/logging/types/audit.ts +31 -0
  576. package/src/logging/types/index.ts +3 -0
  577. package/src/middleware/auth-decision.ts +43 -0
  578. package/src/middleware/create-middleware.ts +626 -0
  579. package/src/middleware/rbac-check.ts +244 -0
  580. package/src/middleware/twofa-presets.ts +224 -0
  581. package/src/models/DecodedAccessToken.ts +17 -0
  582. package/src/models/SessionModel.ts +258 -0
  583. package/src/pages/admin-login/page.tsx +229 -0
  584. package/src/pages/admin-roles/RolesAdminPage.tsx +357 -0
  585. package/src/pages/admin-roles/index.ts +9 -0
  586. package/src/pages/admin-roles/modals.tsx +469 -0
  587. package/src/pages/client-admin/ClientSiteAdminPage.tsx +380 -0
  588. package/src/pages/client-admin/index.ts +33 -0
  589. package/src/pages/login/page.tsx +463 -0
  590. package/src/pages/profile/EnhancedProfilePage.tsx +479 -0
  591. package/src/pages/profile/index.ts +9 -0
  592. package/src/pages/profile/page.tsx +166 -0
  593. package/src/pages/recovery/page.tsx +234 -0
  594. package/src/pages/roles/MyRolesPage.tsx +211 -0
  595. package/src/pages/roles/components.tsx +294 -0
  596. package/src/pages/roles/index.ts +17 -0
  597. package/src/pages/security/EnhancedSecurityPage.tsx +574 -0
  598. package/src/pages/security/index.ts +9 -0
  599. package/src/pages/security/page.tsx +507 -0
  600. package/src/pages/settings/EnhancedSettingsPage.tsx +642 -0
  601. package/src/pages/settings/index.ts +9 -0
  602. package/src/pages/settings/page.tsx +47 -0
  603. package/src/pages/showcase/ShowcasePage.tsx +530 -0
  604. package/src/pages/showcase/index.ts +13 -0
  605. package/src/pages/test-env/EmergencyLogoutPage.tsx +179 -0
  606. package/src/pages/test-env/JwtInspectPage.tsx +418 -0
  607. package/src/pages/test-env/RefreshTokenPage.tsx +155 -0
  608. package/src/pages/test-env/TestEnvPage.tsx +116 -0
  609. package/src/pages/test-env/index.ts +25 -0
  610. package/src/pages/verify-code/page.tsx +648 -0
  611. package/src/routes/account/index.ts +32 -0
  612. package/src/routes/account/masked-info.ts +37 -0
  613. package/src/routes/account/send-code.ts +40 -0
  614. package/src/routes/account/update-phone.ts +13 -0
  615. package/src/routes/account/verify-email.ts +41 -0
  616. package/src/routes/account/verify-sms.ts +41 -0
  617. package/src/routes/auth/index.ts +23 -0
  618. package/src/routes/auth/logout.ts +127 -0
  619. package/src/routes/auth/nextauth.ts +71 -0
  620. package/src/routes/auth/refresh.ts +54 -0
  621. package/src/routes/auth/session.ts +193 -0
  622. package/src/routes/auth/settings.ts +75 -0
  623. package/src/routes/auth/viability.ts +220 -0
  624. package/src/routes/index.ts +18 -0
  625. package/src/routes/session/index.ts +7 -0
  626. package/src/routes/session/refresh-viability.ts +17 -0
  627. package/src/services/signalrActivityService.ts +258 -0
  628. package/src/stores/authStore.ts +1904 -0
  629. package/src/templates/instrumentation.ts +41 -0
  630. package/src/theme/ThemeProvider.tsx +39 -0
  631. package/src/theme/default.ts +33 -0
  632. package/src/theme/index.ts +31 -0
  633. package/src/theme/types.ts +69 -0
  634. package/src/theme/useTheme.ts +57 -0
  635. package/src/theme/utils.ts +40 -0
  636. package/src/types/api.ts +13 -0
  637. package/src/types/auth.d.ts +15 -0
  638. package/src/types/auth.ts +22 -0
  639. package/src/types/logging.ts +11 -0
  640. package/src/types/next-auth.d.ts +15 -0
  641. package/src/types/recovery.ts +54 -0
  642. package/src/types/security.ts +1 -0
  643. package/src/utils/api.ts +353 -0
  644. package/src/utils/circuitBreaker.ts +40 -0
  645. package/src/utils/error-message.ts +108 -0
  646. package/src/utils/layout/reservedSpace.ts +124 -0
  647. package/src/utils/logout.ts +30 -0
  648. package/src/vibe/client.ts +590 -0
  649. package/src/vibe/errors.ts +185 -0
  650. package/src/vibe/generic.ts +429 -0
  651. package/src/vibe/hooks/index.ts +367 -0
  652. package/src/vibe/index.ts +121 -0
  653. package/src/vibe/sessions.ts +551 -0
  654. package/src/vibe/types.ts +577 -0
@@ -0,0 +1,1904 @@
1
+ /**
2
+ * 🚀 CENTRALIZED AUTH STORE - THE SINGLE SOURCE OF TRUTH
3
+ *
4
+ * This Zustand store replaces ALL scattered useState patterns for auth-related state.
5
+ * No more prop drilling, no more duplicate loading states, no more auth chaos.
6
+ *
7
+ * Features:
8
+ * - Centralized session, token, and user state
9
+ * - Built-in API calling with auto token refresh
10
+ * - Loading state management for all async operations
11
+ * - Type-safe throughout
12
+ * - Integrates seamlessly with existing NextAuth
13
+ */
14
+
15
+ import { create } from 'zustand';
16
+ import { devtools } from 'zustand/middleware';
17
+ import { signOut } from 'next-auth/react';
18
+ import { AppSession, isValidSession, sanitizeSession } from '../lib/session';
19
+ import { authLogger } from '../config/logger';
20
+ import { ENV_CONFIG } from '../config/env';
21
+ import { HubConnectionBuilder, HubConnection, HubConnectionState } from '@microsoft/signalr';
22
+ import { signalRActivityService } from '../services/signalrActivityService';
23
+ import {
24
+ getSessionCookieName,
25
+ getSecureSessionCookieName,
26
+ getCsrfCookieName,
27
+ getSecureCsrfCookieName
28
+ } from '../lib/app-slug';
29
+
30
+ // ===============================
31
+ // INTERFACES & TYPES
32
+ // ===============================
33
+
34
+ export interface User {
35
+ id: string;
36
+ email: string;
37
+ roles: string[];
38
+ twoFactorSessionVerified: boolean;
39
+ requiresTwoFactor: boolean;
40
+ twoFactorMethod?: string;
41
+ authenticationMethods?: string[];
42
+ authenticationLevel?: string;
43
+
44
+ // Administrative States (from user_state_management.md)
45
+ isApproved: boolean;
46
+ isSuspended: boolean;
47
+ lockoutEnabled: boolean;
48
+ lockoutEnd?: Date | null;
49
+
50
+ // Suspension Metadata
51
+ pausedAt?: Date | null;
52
+ pausedBy?: string | null;
53
+ suspensionReason?: string | null;
54
+ }
55
+
56
+ // SignalR Event Types
57
+ export interface UserStateChangeEvent {
58
+ userId: string;
59
+ action: 'APPROVE' | 'DISAPPROVE' | 'PAUSE' | 'RESUME' | 'HALT' | 'UNLOCK';
60
+ newState: {
61
+ isApproved?: boolean;
62
+ isSuspended?: boolean;
63
+ lockoutEnabled?: boolean;
64
+ lockoutEnd?: string | null;
65
+ pausedAt?: string | null;
66
+ pausedBy?: string | null;
67
+ suspensionReason?: string | null;
68
+ };
69
+ reason?: string;
70
+ changedBy: string;
71
+ timestamp: string;
72
+ }
73
+
74
+ export interface SecurityNotificationEvent {
75
+ type: 'USER_LOCKOUT' | 'IP_THROTTLE' | 'BRUTE_FORCE' | 'DISTRIBUTED_ATTACK';
76
+ userId?: string;
77
+ ipAddress?: string;
78
+ message: string;
79
+ severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
80
+ timestamp: string;
81
+ }
82
+
83
+ export interface AuthState {
84
+ // Core Auth State
85
+ session: AppSession | null;
86
+ user: User | null;
87
+ accessToken: string | null;
88
+ refreshToken: string | null;
89
+ isAuthenticated: boolean;
90
+ isInitialized: boolean;
91
+
92
+ // Loading States (replaces useState hell)
93
+ isLoading: boolean;
94
+ isRefreshingToken: boolean;
95
+
96
+ // Data Loading States
97
+ isLoadingUserStats: boolean;
98
+ isLoadingClients: boolean;
99
+ isLoadingRoles: boolean;
100
+ isLoadingUsers: boolean;
101
+ isLoadingUserDetails: Record<string, boolean>;
102
+ isLoadingRoleCategories: boolean;
103
+ isLoadingUserAssignments: Record<string, boolean>;
104
+ isLoadingClientAuthorizations: Record<string, boolean>;
105
+
106
+ // Error States
107
+ error: string | null;
108
+ tokenError: string | null;
109
+
110
+ // Cached Data (eliminates redundant API calls)
111
+ userStats: any | null;
112
+ clients: any[] | null;
113
+ roles: any[] | null;
114
+ users: any[] | null;
115
+ userDetails: Record<string, any>;
116
+ userAssignments: Record<string, any>;
117
+ roleCategories: any[] | null;
118
+ clientAuthorizations: Record<string, any[] | undefined>;
119
+
120
+ // Cache timestamps
121
+ userStatsLastFetch: number | null;
122
+ clientsLastFetch: number | null;
123
+ rolesLastFetch: number | null;
124
+ usersLastFetch: number | null;
125
+ roleCategoriesLastFetch: number | null;
126
+
127
+ // Real-time SignalR Connection
128
+ signalrConnection: HubConnection | null;
129
+ signalrConnectionState: HubConnectionState;
130
+ isConnectedToSignalR: boolean;
131
+ }
132
+
133
+ export interface AuthActions {
134
+ // Session Management
135
+ setSession: (session: AppSession | null) => void;
136
+ clearSession: () => void;
137
+ refreshSession: () => Promise<void>;
138
+ refreshTokens: () => Promise<void>;
139
+ rehydrateSessionAfterRefresh: () => Promise<void>;
140
+
141
+ // Authentication Actions
142
+ signIn: (credentials: { email: string; password: string }) => Promise<boolean>;
143
+ signOut: () => Promise<void>;
144
+ forceLogoutAndRedirect: (reason: string) => Promise<void>;
145
+
146
+ // API Actions with Built-in Token Management
147
+ apiCall: <T = any>(url: string, options?: RequestInit, maxRetries?: number) => Promise<T>;
148
+ makeApiCall: <T = any>(url: string, options?: RequestInit, attempt?: number) => Promise<T>;
149
+
150
+ // Data Fetching Actions (replaces useEffect + useState patterns)
151
+ fetchUserStats: (force?: boolean) => Promise<void>;
152
+ fetchClients: (force?: boolean) => Promise<void>;
153
+ fetchRoles: (force?: boolean) => Promise<void>;
154
+ fetchUsers: (params?: any, force?: boolean) => Promise<void>;
155
+ fetchUserDetails: (userId: string, force?: boolean) => Promise<void>;
156
+ fetchUserClientAuthorizations: (userId: string, force?: boolean) => Promise<void>;
157
+ fetchUserRoleAssignments: (userId: string, force?: boolean) => Promise<void>;
158
+ fetchRoleCategories: (force?: boolean) => Promise<void>;
159
+
160
+ // CRUD Operations (replaces direct API calls)
161
+ createUser: (userData: any) => Promise<any>;
162
+ updateUser: (userId: string, updates: any) => Promise<any>;
163
+ deleteUser: (userId: string) => Promise<void>;
164
+ createRole: (roleData: any) => Promise<any>;
165
+ updateRole: (roleId: string, updates: any) => Promise<any>;
166
+ deleteRole: (roleId: string) => Promise<void>;
167
+ assignUserToRole: (userId: string, roleId: string) => Promise<void>;
168
+ removeUserFromRole: (userId: string, roleId: string) => Promise<void>;
169
+ assignUserToClient: (userId: string, clientId: string) => Promise<void>;
170
+ removeUserFromClient: (userId: string, clientId: string) => Promise<void>;
171
+
172
+ // Utility Actions
173
+ hasRole: (role: string) => boolean;
174
+ hasAnyRole: (roles: string[]) => boolean;
175
+ hasAllRoles: (roles: string[]) => boolean;
176
+ isFullyAuthenticated: () => boolean;
177
+
178
+ // Error Management
179
+ setError: (error: string | null) => void;
180
+ clearError: () => void;
181
+
182
+ // Admin User State Management Actions
183
+ approveUser: (userId: string, reason?: string) => Promise<void>;
184
+ disapproveUser: (userId: string, reason?: string) => Promise<void>;
185
+ pauseUser: (userId: string, reason?: string) => Promise<void>;
186
+ resumeUser: (userId: string) => Promise<void>;
187
+ haltUser: (userId: string, reason?: string) => Promise<void>;
188
+ unlockUser: (userId: string) => Promise<void>;
189
+
190
+ // User State Utilities
191
+ canUserAccess: () => boolean;
192
+ getUserStateDisplay: () => string;
193
+ isUserLocked: () => boolean;
194
+
195
+ // Real-time SignalR Management
196
+ initializeSignalR: () => Promise<void>;
197
+ disconnectSignalR: () => Promise<void>;
198
+ handleUserStateChanged: (data: UserStateChangeEvent) => void;
199
+ }
200
+
201
+ export type AuthStore = AuthState & AuthActions;
202
+
203
+ // ===============================
204
+ // CACHE CONFIGURATION
205
+ // ===============================
206
+
207
+ const CACHE_DURATION = {
208
+ USER_STATS: 5 * 60 * 1000, // 5 minutes
209
+ CLIENTS: 10 * 60 * 1000, // 10 minutes
210
+ ROLES: 15 * 60 * 1000, // 15 minutes
211
+ USERS: 5 * 60 * 1000, // 5 minutes (dynamic data)
212
+ USER_DETAILS: 2 * 60 * 1000, // 2 minutes (detailed data)
213
+ ROLE_CATEGORIES: 30 * 60 * 1000, // 30 minutes (stable data)
214
+ USER_ASSIGNMENTS: 2 * 60 * 1000, // 2 minutes (assignment data changes frequently)
215
+ };
216
+
217
+ // ===============================
218
+ // ZUSTAND STORE IMPLEMENTATION
219
+ // ===============================
220
+
221
+ export const useAuthStore = create<AuthStore>()(
222
+ devtools(
223
+ (set, get) => ({
224
+ // ===============================
225
+ // INITIAL STATE
226
+ // ===============================
227
+ session: null,
228
+ user: null,
229
+ accessToken: null,
230
+ refreshToken: null,
231
+ isAuthenticated: false,
232
+ isInitialized: false,
233
+
234
+ // Loading States
235
+ isLoading: true,
236
+ isRefreshingToken: false,
237
+ isLoadingUserStats: false,
238
+ isLoadingClients: false,
239
+ isLoadingRoles: false,
240
+ isLoadingUsers: false,
241
+ isLoadingUserDetails: {},
242
+ isLoadingRoleCategories: false,
243
+ isLoadingUserAssignments: {},
244
+ isLoadingClientAuthorizations: {},
245
+
246
+ // Error States
247
+ error: null,
248
+ tokenError: null,
249
+
250
+ // Cached Data
251
+ userStats: null,
252
+ clients: null,
253
+ roles: null,
254
+ users: null,
255
+ userDetails: {},
256
+ userAssignments: {},
257
+ roleCategories: null,
258
+ clientAuthorizations: {},
259
+
260
+ // Cache Timestamps
261
+ userStatsLastFetch: null,
262
+ clientsLastFetch: null,
263
+ rolesLastFetch: null,
264
+ usersLastFetch: null,
265
+ roleCategoriesLastFetch: null,
266
+
267
+ // SignalR Connection State
268
+ signalrConnection: null,
269
+ signalrConnectionState: HubConnectionState.Disconnected,
270
+ isConnectedToSignalR: false,
271
+
272
+ // ===============================
273
+ // SESSION MANAGEMENT ACTIONS
274
+ // ===============================
275
+
276
+ setSession: (session: AppSession | null) => {
277
+ // CRITICAL FIX: Validate and sanitize the session before setting it
278
+ // This prevents storing sessions with empty user IDs or other invalid data
279
+ const cleanSession = sanitizeSession(session);
280
+
281
+ if (!cleanSession) {
282
+ // Session is invalid (empty userId, empty email, or missing accessToken)
283
+ authLogger.warn('[AuthStore] Rejecting invalid session', {
284
+ hasSession: false,
285
+ reason: 'invalid_or_partial_session',
286
+ hasIncomingSession: !!session,
287
+ incomingUserId: session?.user?.id || '(empty)',
288
+ incomingUserEmail: session?.user?.email || '(empty)',
289
+ hasAccessToken: !!session?.accessToken
290
+ });
291
+
292
+ // Clear the session state completely
293
+ set({
294
+ session: null,
295
+ user: null,
296
+ accessToken: null,
297
+ refreshToken: null,
298
+ isAuthenticated: false,
299
+ isInitialized: true,
300
+ isLoading: false,
301
+ error: 'Invalid session data',
302
+ });
303
+
304
+ return;
305
+ }
306
+
307
+ // Session is valid - proceed with setting it
308
+ authLogger.info('[AuthStore] Setting valid session:', {
309
+ hasSession: true,
310
+ userId: cleanSession.user?.id,
311
+ userEmail: cleanSession.user?.email
312
+ });
313
+
314
+ const user: User = {
315
+ id: cleanSession.user?.id || '',
316
+ email: cleanSession.user?.email || '',
317
+ roles: Array.isArray((cleanSession.user as any)?.roles) ? (cleanSession.user as any).roles : [],
318
+ twoFactorSessionVerified: (cleanSession.user as any)?.twoFactorSessionVerified || false,
319
+ requiresTwoFactor: (cleanSession.user as any)?.requiresTwoFactor || false,
320
+ twoFactorMethod: (cleanSession.user as any)?.twoFactorMethod,
321
+ authenticationMethods: (cleanSession.user as any).authenticationMethods,
322
+ authenticationLevel: (cleanSession.user as any).authenticationLevel,
323
+
324
+ // Administrative States (with safe defaults)
325
+ isApproved: (cleanSession.user as any).isApproved ?? true,
326
+ isSuspended: (cleanSession.user as any).isSuspended ?? false,
327
+ lockoutEnabled: (cleanSession.user as any).lockoutEnabled ?? false,
328
+ lockoutEnd: (cleanSession.user as any).lockoutEnd ? new Date((cleanSession.user as any).lockoutEnd) : null,
329
+
330
+ // Suspension Metadata
331
+ pausedAt: (cleanSession.user as any).pausedAt ? new Date((cleanSession.user as any).pausedAt) : null,
332
+ pausedBy: (cleanSession.user as any).pausedBy || null,
333
+ suspensionReason: (cleanSession.user as any).suspensionReason || null,
334
+ };
335
+
336
+ set({
337
+ session: cleanSession,
338
+ user,
339
+ accessToken: cleanSession.accessToken || null,
340
+ refreshToken: cleanSession.refreshToken || null,
341
+ // FIXED: Use strict validation - both accessToken AND user.id must be non-empty
342
+ isAuthenticated: true, // Already validated by sanitizeSession
343
+ isInitialized: true,
344
+ isLoading: false,
345
+ error: cleanSession.error || null,
346
+ });
347
+
348
+ // Auto-initialize SignalR for authenticated users
349
+ // Use a longer delay and add error handling to prevent login blocking
350
+ setTimeout(async () => {
351
+ try {
352
+ await get().initializeSignalR();
353
+ } catch (error) {
354
+ // Don't let SignalR initialization errors block the login process
355
+ authLogger.warn('[AuthStore] SignalR initialization failed (non-blocking):', error);
356
+ }
357
+ }, 500); // Longer delay to ensure login flow completes first
358
+ },
359
+
360
+ clearSession: () => {
361
+ authLogger.debug('[AuthStore] Clearing session');
362
+
363
+ // Disconnect SignalR before clearing session
364
+ get().disconnectSignalR();
365
+
366
+ set({
367
+ session: null,
368
+ user: null,
369
+ accessToken: null,
370
+ refreshToken: null,
371
+ isAuthenticated: false,
372
+ error: null,
373
+ tokenError: null,
374
+ // Keep cache data but clear sensitive auth data
375
+ });
376
+ },
377
+
378
+ refreshSession: async () => {
379
+ const state = get();
380
+ if (!state.session?.sessionToken || !state.user?.id || state.isRefreshingToken) {
381
+ return;
382
+ }
383
+
384
+ set({ isRefreshingToken: true, tokenError: null });
385
+
386
+ try {
387
+ // Call refresh endpoint directly (no middleware chaos)
388
+ const stateBefore = get();
389
+ const refreshResponse = await fetch('/api/auth/refresh', {
390
+ method: 'POST',
391
+ headers: {
392
+ 'Content-Type': 'application/json',
393
+ 'X-Session-Token': stateBefore.session?.sessionToken || '',
394
+ 'X-Request-Source': 'authStore.refreshSession'
395
+ },
396
+ credentials: 'include'
397
+ });
398
+
399
+ if (!refreshResponse.ok) {
400
+ const refreshError = await refreshResponse.json().catch(() => ({ message: 'Token refresh failed' }));
401
+ throw new Error(refreshError.message || 'Token refresh failed');
402
+ }
403
+
404
+ // TODO: Get updated session from Redis after successful refresh (temp disabled for client build)
405
+ // For now, assume refresh was successful
406
+
407
+ // TODO: Update session with new tokens from Redis (temp disabled)
408
+ // For now, assume session tokens are already updated by the refresh endpoint
409
+
410
+ authLogger.info('[AuthStore] Token refresh successful via direct /api/session/refresh');
411
+ } catch (error) {
412
+ const errorMessage = error instanceof Error ? error.message : 'Token refresh failed';
413
+ authLogger.error('[AuthStore] Token refresh failed:', error);
414
+
415
+ set({
416
+ tokenError: errorMessage,
417
+ isAuthenticated: false,
418
+ });
419
+
420
+ // If refresh fails, sign out
421
+ await get().signOut();
422
+ } finally {
423
+ set({ isRefreshingToken: false });
424
+ }
425
+ },
426
+
427
+ // ===============================
428
+ // AUTHENTICATION ACTIONS
429
+ // ===============================
430
+
431
+ signIn: async (credentials: { email: string; password: string }) => {
432
+ set({ isLoading: true, error: null });
433
+
434
+ try {
435
+ // Use NextAuth signIn - this will trigger our auth callbacks
436
+ const { signIn } = await import('next-auth/react');
437
+ const result = await signIn('credentials', {
438
+ ...credentials,
439
+ redirect: false,
440
+ });
441
+
442
+ if (result?.ok) {
443
+ authLogger.info('[AuthStore] Sign in successful');
444
+ return true;
445
+ } else {
446
+ const errorMessage = result?.error || 'Sign in failed';
447
+ set({ error: errorMessage, isLoading: false });
448
+ return false;
449
+ }
450
+ } catch (error) {
451
+ const errorMessage = error instanceof Error ? error.message : 'Sign in failed';
452
+ authLogger.error('[AuthStore] Sign in error:', error);
453
+ set({ error: errorMessage, isLoading: false });
454
+ return false;
455
+ }
456
+ },
457
+
458
+ signOut: async () => {
459
+ authLogger.info('[AuthStore] Starting sign out process');
460
+
461
+ // Clear local state immediately
462
+ get().clearSession();
463
+
464
+ // Clear cached data
465
+ set({
466
+ userStats: null,
467
+ clients: null,
468
+ roles: null,
469
+ userStatsLastFetch: null,
470
+ clientsLastFetch: null,
471
+ rolesLastFetch: null,
472
+ });
473
+
474
+ try {
475
+ // Use NextAuth signOut
476
+ await signOut({ redirect: false });
477
+ authLogger.info('[AuthStore] Sign out completed');
478
+ } catch (error) {
479
+ authLogger.error('[AuthStore] Sign out error:', error);
480
+ // Even if signOut fails, we've cleared local state
481
+ }
482
+ },
483
+
484
+ forceLogoutAndRedirect: async (reason: string) => {
485
+ const state = get();
486
+
487
+ // AGGRESSIVE LOGGING TO TRACK EXECUTION
488
+ console.error('🚨 FORCE LOGOUT INITIATED 🚨', {
489
+ reason,
490
+ userId: state.user?.id,
491
+ sessionToken: state.session?.sessionToken,
492
+ timestamp: new Date().toISOString()
493
+ });
494
+
495
+ authLogger.error('[AuthStore] Force logout initiated', {
496
+ reason,
497
+ userId: state.user?.id,
498
+ sessionToken: state.session?.sessionToken
499
+ });
500
+
501
+ try {
502
+ // TODO: Step 1: Mark session as force-invalidated in Redis (temp disabled for client build)
503
+
504
+ // Step 2: Clear local state immediately
505
+ get().clearSession();
506
+
507
+ // Step 3: Clear all cached data
508
+ set({
509
+ userStats: null,
510
+ clients: null,
511
+ roles: null,
512
+ userStatsLastFetch: null,
513
+ clientsLastFetch: null,
514
+ rolesLastFetch: null,
515
+ });
516
+
517
+ // Step 4: Clear session cache (temp disabled for client build)
518
+ // TODO: Re-enable when session-cache module is available
519
+ // const { SessionCache } = await import('@/lib/session-cache');
520
+ // SessionCache.clearCache();
521
+ // SessionCache.clearServerSessionData();
522
+
523
+ // Step 5: AGGRESSIVELY clear NextAuth cookies immediately before signOut (app-slug prefixed)
524
+ if (typeof document !== 'undefined') {
525
+ const cookiesToClear = [
526
+ getSessionCookieName(),
527
+ getSecureSessionCookieName(),
528
+ getCsrfCookieName(),
529
+ getSecureCsrfCookieName()
530
+ ];
531
+
532
+ authLogger.info('[AuthStore] Aggressively clearing NextAuth cookies immediately');
533
+
534
+ cookiesToClear.forEach(cookieName => {
535
+ try {
536
+ const expiredDate = 'Thu, 01 Jan 1970 00:00:00 GMT';
537
+ const clearPatterns = [
538
+ `${cookieName}=; expires=${expiredDate}; path=/`,
539
+ `${cookieName}=; expires=${expiredDate}; path=/; domain=${window.location.hostname}`,
540
+ `${cookieName}=; expires=${expiredDate}; path=/; domain=.${window.location.hostname}`,
541
+ ];
542
+
543
+ if (window.location.protocol === 'https:') {
544
+ clearPatterns.push(
545
+ `${cookieName}=; expires=${expiredDate}; path=/; secure`,
546
+ `${cookieName}=; expires=${expiredDate}; path=/; domain=${window.location.hostname}; secure`,
547
+ `${cookieName}=; expires=${expiredDate}; path=/; domain=.${window.location.hostname}; secure`
548
+ );
549
+ }
550
+
551
+ clearPatterns.forEach(pattern => {
552
+ document.cookie = pattern;
553
+ });
554
+ } catch (cookieError) {
555
+ authLogger.warn(`Failed to clear cookie ${cookieName}:`, cookieError);
556
+ }
557
+ });
558
+ }
559
+
560
+ // Step 6: Force NextAuth signOut (this should clear the session cookie)
561
+ const { signOut } = await import('next-auth/react');
562
+ await signOut({ redirect: false });
563
+
564
+ authLogger.info('[AuthStore] Force logout completed, redirecting to login');
565
+
566
+ // Step 7: Longer delay to ensure everything is processed
567
+ await new Promise(resolve => setTimeout(resolve, 1000));
568
+
569
+ // Step 8: Force redirect with cache busting - use window.location.href for immediate effect
570
+ const loginUrl = `/account-auth/login?error=SessionExpired&reason=${reason}&t=${Date.now()}`;
571
+ authLogger.info('[AuthStore] Redirecting to login:', loginUrl);
572
+
573
+ // Double redirect approach to ensure it works
574
+ window.location.replace(loginUrl);
575
+ window.location.href = loginUrl;
576
+
577
+ } catch (error) {
578
+ authLogger.error('[AuthStore] Error during force logout:', error);
579
+
580
+ // Fallback: still redirect even if cleanup fails
581
+ const loginUrl = `/account-auth/login?error=ForceLogoutError&reason=${reason}&t=${Date.now()}`;
582
+ window.location.replace(loginUrl);
583
+ }
584
+ },
585
+
586
+ // ===============================
587
+ // API CALLING WITH AUTO TOKEN REFRESH
588
+ // ===============================
589
+
590
+ // Single centralized refresh method with coordinated server-side coordination
591
+ refreshTokens: async (): Promise<void> => {
592
+ const state = get();
593
+
594
+ // If already refreshing, wait for it to complete
595
+ if (state.isRefreshingToken) {
596
+ authLogger.info('[AuthStore] Client-side token refresh already in progress, waiting for completion');
597
+
598
+ // Wait for the refresh to complete by polling the flag
599
+ const maxWaitMs = 15000; // 15 seconds max wait
600
+ const startTime = Date.now();
601
+
602
+ while (get().isRefreshingToken && (Date.now() - startTime) < maxWaitMs) {
603
+ await new Promise(resolve => setTimeout(resolve, 100));
604
+ }
605
+
606
+ // Check if refresh was successful by verifying we still have a valid session
607
+ const updatedState = get();
608
+ if (updatedState.isRefreshingToken) {
609
+ authLogger.warn('[AuthStore] Client-side token refresh timed out');
610
+ throw new Error('Token refresh timed out - another refresh operation is stuck');
611
+ }
612
+
613
+ if (!updatedState.isAuthenticated || !updatedState.session) {
614
+ authLogger.error('[AuthStore] Token refresh failed - session invalid after refresh');
615
+ throw new Error('Token refresh failed - session invalid after refresh');
616
+ }
617
+
618
+ authLogger.info('[AuthStore] Token refresh completed by another caller');
619
+ return;
620
+ }
621
+
622
+ // Set refreshing flag for client-side coordination
623
+ set({ isRefreshingToken: true });
624
+ authLogger.info('[AuthStore] Starting coordinated token refresh');
625
+
626
+ try {
627
+ // COORDINATED REFRESH: The server now handles distributed locking
628
+ // We just need to make the refresh call and let the server coordinate
629
+
630
+ // Resolve session token for header
631
+ const resolveSessionTokenForHeader = async (): Promise<string | undefined> => {
632
+ let token = get().session?.sessionToken as string | undefined;
633
+ if (token) return token;
634
+ try {
635
+ const { getSession } = await import('next-auth/react');
636
+ for (let attempt = 1; attempt <= 3 && !token; attempt++) {
637
+ const s: any = await getSession();
638
+ token = s?.sessionToken;
639
+ if (!token) {
640
+ await new Promise(r => setTimeout(r, 150));
641
+ }
642
+ }
643
+ } catch {
644
+ authLogger.warn('[AuthStore] Failed to resolve session token from NextAuth during refresh');
645
+ }
646
+ return token;
647
+ };
648
+
649
+ const sessionTokenHeader = await resolveSessionTokenForHeader();
650
+
651
+ const refreshResponse = await fetch('/api/auth/refresh', {
652
+ method: 'POST',
653
+ headers: {
654
+ 'Content-Type': 'application/json',
655
+ 'X-Client-Refresh': 'true',
656
+ 'X-Request-Source': 'authStore',
657
+ ...(sessionTokenHeader ? { 'X-Session-Token': sessionTokenHeader } : {})
658
+ },
659
+ credentials: 'include'
660
+ });
661
+
662
+ if (!refreshResponse.ok) {
663
+ if (refreshResponse.status === 429 || refreshResponse.status === 409) {
664
+ const retryAfter = refreshResponse.headers.get('Retry-After');
665
+ const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 2000;
666
+ authLogger.info('[AuthStore] Server coordinated refresh in progress, waiting and retrying', {
667
+ status: refreshResponse.status,
668
+ retryAfterMs: waitMs
669
+ });
670
+ await new Promise(resolve => setTimeout(resolve, Math.min(waitMs, 5000)));
671
+ await get().rehydrateSessionAfterRefresh();
672
+ authLogger.info('[AuthStore] Successfully coordinated with server-side refresh');
673
+ return;
674
+ }
675
+
676
+ const refreshError = await refreshResponse.json().catch(() => ({ message: 'Token refresh failed' }));
677
+ authLogger.error('[AuthStore] Token refresh failed:', refreshError);
678
+
679
+ set({
680
+ session: null,
681
+ accessToken: null,
682
+ refreshToken: null,
683
+ isAuthenticated: false,
684
+ user: null
685
+ });
686
+
687
+ await get().forceLogoutAndRedirect('TokenRefreshFailed');
688
+ throw new Error(`Token refresh failed: ${refreshError.message}`);
689
+ }
690
+
691
+ authLogger.info('[AuthStore] Coordinated token refresh successful on backend, rehydrating session');
692
+ await get().rehydrateSessionAfterRefresh();
693
+
694
+ } catch (error) {
695
+ authLogger.error('[AuthStore] Coordinated token refresh exception:', error);
696
+
697
+ const errorMessage = error instanceof Error ? error.message : String(error);
698
+ const isCoordinationError = errorMessage.includes('coordination') ||
699
+ errorMessage.includes('timeout') ||
700
+ errorMessage.includes('another refresh');
701
+
702
+ if (!isCoordinationError) {
703
+ set({
704
+ session: null,
705
+ accessToken: null,
706
+ refreshToken: null,
707
+ isAuthenticated: false,
708
+ user: null
709
+ });
710
+ await get().forceLogoutAndRedirect('TokenRefreshException');
711
+ }
712
+
713
+ throw error;
714
+ } finally {
715
+ set({ isRefreshingToken: false });
716
+ }
717
+ },
718
+
719
+ // Atomic session rehydration after token refresh
720
+ // This ensures the authStore gets the fresh tokens from the session store
721
+ rehydrateSessionAfterRefresh: async (): Promise<void> => {
722
+ const maxRetries = 3;
723
+ const retryDelay = 500; // 500ms between retries
724
+
725
+ authLogger.info('[AuthStore] Starting session rehydration after token refresh');
726
+
727
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
728
+ try {
729
+ // Force NextAuth to reload the session from the session store
730
+ // This triggers the JWT callback which reads fresh data from Redis
731
+ const { getSession } = await import('next-auth/react');
732
+
733
+ authLogger.debug(`[AuthStore] Rehydration attempt ${attempt}/${maxRetries}`);
734
+
735
+ // Force session refresh - this calls NextAuth's session endpoint
736
+ // which triggers JWT callback to read fresh tokens from Redis
737
+ const freshSession = await getSession();
738
+
739
+ if (!freshSession) {
740
+ throw new Error('No session returned from NextAuth after refresh');
741
+ }
742
+
743
+ if (!freshSession.accessToken) {
744
+ throw new Error('Fresh session missing access token');
745
+ }
746
+
747
+ // Verify the token is actually fresh (not expired)
748
+ try {
749
+ const { jwtDecode } = await import('@/lib/jwt-decode');
750
+ const decoded = jwtDecode(freshSession.accessToken);
751
+
752
+ if (!decoded?.exp) {
753
+ throw new Error('Fresh token missing expiration claim');
754
+ }
755
+
756
+ const tokenExpiry = decoded.exp * 1000;
757
+ const now = Date.now();
758
+ const timeUntilExpiry = tokenExpiry - now;
759
+
760
+ // Token should be fresh (not expiring in next 5 minutes)
761
+ if (timeUntilExpiry < (5 * 60 * 1000)) {
762
+ throw new Error(`Fresh token still expires soon: ${timeUntilExpiry}ms`);
763
+ }
764
+
765
+ authLogger.info('[AuthStore] Session rehydration successful', {
766
+ attempt,
767
+ tokenExpiresAt: new Date(tokenExpiry).toISOString(),
768
+ timeUntilExpiry: `${Math.round(timeUntilExpiry / 1000)}s`,
769
+ subject: decoded.sub
770
+ });
771
+
772
+ // Update the authStore with the fresh session atomically
773
+ get().setSession(freshSession);
774
+
775
+ authLogger.info('[AuthStore] AuthStore updated with fresh tokens');
776
+ return; // Success!
777
+
778
+ } catch (tokenError) {
779
+ throw new Error(`Token validation failed: ${tokenError instanceof Error ? tokenError.message : String(tokenError)}`);
780
+ }
781
+
782
+ } catch (error) {
783
+ const errorMessage = error instanceof Error ? error.message : String(error);
784
+ authLogger.warn(`[AuthStore] Rehydration attempt ${attempt} failed:`, errorMessage);
785
+
786
+ // If this is the last attempt, throw the error
787
+ if (attempt === maxRetries) {
788
+ authLogger.error('[AuthStore] Session rehydration failed after all retries');
789
+ throw new Error(`Session rehydration failed after ${maxRetries} attempts: ${errorMessage}`);
790
+ }
791
+
792
+ // Wait before retrying
793
+ authLogger.debug(`[AuthStore] Waiting ${retryDelay}ms before retry ${attempt + 1}`);
794
+ await new Promise(resolve => setTimeout(resolve, retryDelay));
795
+ }
796
+ }
797
+ },
798
+
799
+ apiCall: async <T = any>(url: string, options: RequestInit = {}, maxRetries = 3): Promise<T> => {
800
+ // UNIVERSAL RETRY WRAPPER: Handle 503 retries at the store level
801
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
802
+ try {
803
+ return await get().makeApiCall<T>(url, options, attempt);
804
+ } catch (error: any) {
805
+ const isRetryableError = error.isRetryable || error.message?.includes('Token refresh in progress');
806
+
807
+ if (isRetryableError && attempt < maxRetries) {
808
+ const retryAfter = error.retryAfter || 1;
809
+ authLogger.info(`[AuthStore] API call attempt ${attempt} failed with retryable error, retrying in ${retryAfter}s`, {
810
+ url,
811
+ error: error.message,
812
+ attempt,
813
+ maxRetries
814
+ });
815
+ await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
816
+ continue;
817
+ }
818
+
819
+ // Not retryable or max attempts reached
820
+ throw error;
821
+ }
822
+ }
823
+
824
+ // This should never be reached but TypeScript needs it
825
+ throw new Error('API call failed after all retry attempts');
826
+ },
827
+
828
+ makeApiCall: async <T = any>(url: string, options: RequestInit = {}, attempt = 1): Promise<T> => {
829
+ const state = get();
830
+
831
+ // COORDINATED AUTH: Check authentication and refresh coordination
832
+ if (!state.isAuthenticated || !state.session?.sessionToken || !state.user?.id) {
833
+ throw new Error('Not authenticated');
834
+ }
835
+
836
+ // COORDINATED REFRESH: Check if token refresh is in progress
837
+ if (state.isRefreshingToken) {
838
+ authLogger.info('[AuthStore] API call detected refresh in progress, waiting for completion', { url, attempt });
839
+
840
+ // Wait for refresh to complete before making API call
841
+ const maxWaitMs = 10000; // 10 seconds max wait
842
+ const startTime = Date.now();
843
+
844
+ while (get().isRefreshingToken && (Date.now() - startTime) < maxWaitMs) {
845
+ await new Promise(resolve => setTimeout(resolve, 100));
846
+ }
847
+
848
+ const updatedState = get();
849
+ if (updatedState.isRefreshingToken) {
850
+ authLogger.warn('[AuthStore] API call timed out waiting for refresh', { url, attempt });
851
+ throw new Error('Request failed - token refresh in progress');
852
+ }
853
+
854
+ if (!updatedState.isAuthenticated || !updatedState.session) {
855
+ authLogger.error('[AuthStore] Session lost during refresh wait', { url, attempt });
856
+ throw new Error('Authentication lost during token refresh');
857
+ }
858
+
859
+ authLogger.info('[AuthStore] API call proceeding after refresh completion', { url, attempt });
860
+ }
861
+
862
+ // COORDINATED API CALL: Make API call using NextAuth session cookies
863
+ // Server-side handlers will coordinate any needed token refresh
864
+ const response = await fetch(url, {
865
+ ...options,
866
+ credentials: 'include', // Ensure session cookies are sent
867
+ headers: {
868
+ 'Content-Type': 'application/json',
869
+ 'X-Client-Call': 'true', // Indicate this is a client-initiated call
870
+ 'X-Request-Source': 'authStore',
871
+ ...options.headers,
872
+ },
873
+ });
874
+
875
+ if (!response.ok) {
876
+ // COORDINATED ERROR HANDLING: Handle various coordination scenarios
877
+ if (response.status === 401) {
878
+ authLogger.info('[AuthStore] Received 401, attempting coordinated refresh and retry', { url, attempt });
879
+
880
+ // If not currently refreshing and we haven't retried yet, attempt refresh then retry once
881
+ if (!get().isRefreshingToken && (attempt ?? 1) < 2) {
882
+ try {
883
+ await get().refreshTokens();
884
+ authLogger.info('[AuthStore] Refresh complete, retrying API call once', { url, nextAttempt: (attempt ?? 1) + 1 });
885
+ return await get().makeApiCall<T>(url, options, (attempt ?? 1) + 1);
886
+ } catch (refreshError) {
887
+ authLogger.error('[AuthStore] Refresh attempt failed after 401', refreshError);
888
+ // fall through to force logout below
889
+ }
890
+ } else if (get().isRefreshingToken) {
891
+ authLogger.info('[AuthStore] 401 received during in-progress refresh - treating as coordination issue');
892
+ }
893
+
894
+ // If we reach here, refresh was not attempted or failed; force logout
895
+ await get().forceLogoutAndRedirect('ApiCall401');
896
+ throw new Error('Authentication failed');
897
+ }
898
+
899
+ // Handle server coordination responses
900
+ if (response.status === 503) {
901
+ const retryAfter = response.headers.get('Retry-After');
902
+ authLogger.info('[AuthStore] Received 503 (service unavailable), likely token refresh in progress', {
903
+ url,
904
+ retryAfter
905
+ });
906
+
907
+ // This is a retryable condition - let the calling code handle retries
908
+ const error = new Error('Token refresh in progress');
909
+ (error as any).retryAfter = parseInt(retryAfter || '1', 10);
910
+ (error as any).isRetryable = true;
911
+ throw error;
912
+ }
913
+
914
+ if (response.status === 429) {
915
+ const retryAfter = response.headers.get('Retry-After');
916
+ authLogger.info('[AuthStore] Received 429 (rate limit/coordination), server busy', {
917
+ url,
918
+ retryAfter
919
+ });
920
+ throw new Error(`Server busy - retry after ${retryAfter || '1'} second(s)`);
921
+ }
922
+
923
+ if (response.status === 409) {
924
+ // Distinguish business conflict (e.g., duplicate email/username) from refresh coordination
925
+ let body: any = null;
926
+ try {
927
+ body = await response.clone().json();
928
+ } catch {}
929
+
930
+ const code = body?.error?.code || body?.code;
931
+ const message: string | undefined = body?.error?.message || body?.message;
932
+ const msgLower = typeof message === 'string' ? message.toLowerCase() : '';
933
+
934
+ // PayEz-standard conflicts we want to surface to the UI
935
+ if (
936
+ code === 'RESOURCE_CONFLICT' ||
937
+ code === 'USERNAME_ALREADY_EXISTS' ||
938
+ code === 'EMAIL_ALREADY_EXISTS' ||
939
+ (msgLower.includes('duplicate') || msgLower.includes('already taken'))
940
+ ) {
941
+ const err = new Error(message || 'Resource conflict');
942
+ (err as any).status = 409;
943
+ (err as any).data = body;
944
+ throw err;
945
+ }
946
+
947
+ // Otherwise treat as coordination conflict (e.g., refresh lock)
948
+ authLogger.info('[AuthStore] Received 409 (conflict) without recognizable business code; treating as refresh coordination issue', { url });
949
+ const err = new Error('Request conflict - token refresh in progress on server');
950
+ (err as any).status = 409;
951
+ (err as any).data = body;
952
+ throw err;
953
+ }
954
+
955
+ // Handle other HTTP errors: throw an error with details
956
+ const errorData = await response.json().catch(() => ({}));
957
+ const errorMessage = errorData.message || errorData.error || response.statusText || 'Request failed';
958
+ const error = new Error(errorMessage);
959
+ (error as any).status = response.status;
960
+ (error as any).data = errorData;
961
+ throw error;
962
+ }
963
+
964
+ return await response.json();
965
+ },
966
+
967
+ // ===============================
968
+ // DATA FETCHING ACTIONS
969
+ // ===============================
970
+
971
+ fetchUserStats: async (force = false) => {
972
+ const state = get();
973
+ const now = Date.now();
974
+
975
+ // Check cache unless forced
976
+ if (!force && state.userStats && state.userStatsLastFetch) {
977
+ const age = now - state.userStatsLastFetch;
978
+ if (age < CACHE_DURATION.USER_STATS) {
979
+ authLogger.debug('[AuthStore] Using cached user stats');
980
+ return;
981
+ }
982
+ }
983
+
984
+ set({ isLoadingUserStats: true });
985
+
986
+ try {
987
+ const data = await get().apiCall('/api/activity/user-stats');
988
+ set({
989
+ userStats: data.data || data,
990
+ userStatsLastFetch: now,
991
+ isLoadingUserStats: false,
992
+ });
993
+ authLogger.debug('[AuthStore] User stats fetched successfully');
994
+ } catch (error) {
995
+ const errorMessage = error instanceof Error ? error.message : String(error);
996
+
997
+ // Don't log errors if we're already signed out (forced logout scenario)
998
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
999
+ authLogger.error('[AuthStore] Failed to fetch user stats:', error);
1000
+ }
1001
+
1002
+ set({ isLoadingUserStats: false });
1003
+
1004
+ // Don't throw if we're in a forced logout scenario
1005
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1006
+ throw error;
1007
+ }
1008
+ }
1009
+ },
1010
+
1011
+ fetchClients: async (force = false) => {
1012
+ const state = get();
1013
+ const now = Date.now();
1014
+
1015
+ // Check cache unless forced
1016
+ if (!force && state.clients && state.clientsLastFetch) {
1017
+ const age = now - state.clientsLastFetch;
1018
+ if (age < CACHE_DURATION.CLIENTS) {
1019
+ authLogger.debug('[AuthStore] Using cached clients');
1020
+ return;
1021
+ }
1022
+ }
1023
+
1024
+ set({ isLoadingClients: true });
1025
+
1026
+ try {
1027
+ const data = await get().apiCall('/api/admin/clients');
1028
+ // API returns pure array - no envelope
1029
+ set({
1030
+ clients: Array.isArray(data) ? data : [],
1031
+ clientsLastFetch: now,
1032
+ isLoadingClients: false,
1033
+ });
1034
+ authLogger.debug('[AuthStore] Clients fetched successfully');
1035
+ } catch (error) {
1036
+ const errorMessage = error instanceof Error ? error.message : String(error);
1037
+
1038
+ // Don't log errors if we're already signed out (forced logout scenario)
1039
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1040
+ authLogger.error('[AuthStore] Failed to fetch clients:', error);
1041
+ }
1042
+
1043
+ set({ isLoadingClients: false });
1044
+
1045
+ // Don't throw if we're in a forced logout scenario
1046
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1047
+ throw error;
1048
+ }
1049
+ }
1050
+ },
1051
+
1052
+ fetchRoles: async (force = false) => {
1053
+ const state = get();
1054
+ const now = Date.now();
1055
+
1056
+ // Check cache unless forced
1057
+ if (!force && state.roles && state.rolesLastFetch) {
1058
+ const age = now - state.rolesLastFetch;
1059
+ if (age < CACHE_DURATION.ROLES) {
1060
+ authLogger.debug('[AuthStore] Using cached roles');
1061
+ return;
1062
+ }
1063
+ }
1064
+
1065
+ set({ isLoadingRoles: true });
1066
+
1067
+ try {
1068
+ const data = await get().apiCall('/api/admin/roles');
1069
+ // API returns pure array - no envelope
1070
+ set({
1071
+ roles: Array.isArray(data) ? data : [],
1072
+ rolesLastFetch: now,
1073
+ isLoadingRoles: false,
1074
+ });
1075
+ authLogger.debug('[AuthStore] Roles fetched successfully');
1076
+ } catch (error) {
1077
+ const errorMessage = error instanceof Error ? error.message : String(error);
1078
+
1079
+ // Don't log errors if we're already signed out (forced logout scenario)
1080
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1081
+ authLogger.error('[AuthStore] Failed to fetch roles:', error);
1082
+ }
1083
+
1084
+ set({ isLoadingRoles: false });
1085
+
1086
+ // Don't throw if we're in a forced logout scenario
1087
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1088
+ throw error;
1089
+ }
1090
+ }
1091
+ },
1092
+
1093
+ fetchUsers: async (params: any = {}, force = false) => {
1094
+ const state = get();
1095
+ const now = Date.now();
1096
+
1097
+ // Check cache unless forced (users data changes frequently, shorter cache)
1098
+ if (!force && state.users && state.usersLastFetch) {
1099
+ const age = now - state.usersLastFetch;
1100
+ if (age < CACHE_DURATION.USERS) {
1101
+ authLogger.debug('[AuthStore] Using cached users');
1102
+ return;
1103
+ }
1104
+ }
1105
+
1106
+ set({ isLoadingUsers: true });
1107
+
1108
+ try {
1109
+ // Always use POST for /api/admin/users with appropriate payload
1110
+ const url = `/api/admin/users`;
1111
+ // Build proper UserGridRequest with all required fields
1112
+ const gridParams = {
1113
+ page_number: params.page || 1,
1114
+ page_size: params.pageSize || 25,
1115
+ search: "",
1116
+ search_field: "",
1117
+ sort_field: "",
1118
+ sort_order: "",
1119
+ status: null,
1120
+ merchant_id: "",
1121
+ client_assignment_status: null
1122
+ };
1123
+ const options = { method: 'POST', body: JSON.stringify(gridParams) };
1124
+
1125
+ const data = await get().apiCall(url, options);
1126
+
1127
+ // API returns pure array - no envelope
1128
+ const users = Array.isArray(data) ? data : [];
1129
+
1130
+ set({
1131
+ users: Array.isArray(users) ? users : [],
1132
+ usersLastFetch: now,
1133
+ isLoadingUsers: false,
1134
+ });
1135
+ authLogger.debug('[AuthStore] Users fetched successfully');
1136
+ } catch (error) {
1137
+ const errorMessage = error instanceof Error ? error.message : String(error);
1138
+ authLogger.error('[AuthStore] Failed to fetch users:', error);
1139
+ set({ isLoadingUsers: false });
1140
+
1141
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1142
+ throw error;
1143
+ }
1144
+ }
1145
+ },
1146
+
1147
+ fetchUserDetails: async (userId: string, force = false) => {
1148
+ const state = get();
1149
+ const now = Date.now();
1150
+
1151
+ // Check cache unless forced
1152
+ const cacheKey = `user_${userId}`;
1153
+ const lastFetch = state.userDetails[`${cacheKey}_lastFetch`];
1154
+ if (!force && state.userDetails[userId] && lastFetch) {
1155
+ const age = now - lastFetch;
1156
+ if (age < CACHE_DURATION.USER_DETAILS) {
1157
+ authLogger.debug(`[AuthStore] Using cached user details for ${userId}`);
1158
+ return;
1159
+ }
1160
+ }
1161
+
1162
+ set({
1163
+ isLoadingUserDetails: { ...state.isLoadingUserDetails, [userId]: true }
1164
+ });
1165
+
1166
+ try {
1167
+ const data = await get().apiCall(`/api/admin/users/${userId}`);
1168
+
1169
+ set({
1170
+ userDetails: {
1171
+ ...state.userDetails,
1172
+ [userId]: data.data || data,
1173
+ [`${cacheKey}_lastFetch`]: now
1174
+ },
1175
+ isLoadingUserDetails: { ...state.isLoadingUserDetails, [userId]: false }
1176
+ });
1177
+ authLogger.debug(`[AuthStore] User details fetched for ${userId}`);
1178
+ } catch (error) {
1179
+ const errorMessage = error instanceof Error ? error.message : String(error);
1180
+ authLogger.error(`[AuthStore] Failed to fetch user details for ${userId}:`, error);
1181
+
1182
+ set({
1183
+ isLoadingUserDetails: { ...state.isLoadingUserDetails, [userId]: false }
1184
+ });
1185
+
1186
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1187
+ throw error;
1188
+ }
1189
+ }
1190
+ },
1191
+
1192
+ fetchUserClientAuthorizations: async (userId: string, force = false) => {
1193
+ const state = get();
1194
+ const now = Date.now();
1195
+ const cacheKey = `auth_${userId}`;
1196
+
1197
+ // Check if already loading
1198
+ if (state.isLoadingClientAuthorizations[userId]) {
1199
+ return;
1200
+ }
1201
+
1202
+ // Check cache unless forced
1203
+ if (!force && state.clientAuthorizations[userId]) {
1204
+ authLogger.debug(`[AuthStore] Using cached client authorizations for ${userId}`);
1205
+ return;
1206
+ }
1207
+
1208
+ set({
1209
+ isLoadingClientAuthorizations: { ...state.isLoadingClientAuthorizations, [userId]: true }
1210
+ });
1211
+
1212
+ try {
1213
+ const data = await get().apiCall(`/api/admin/users/client-authorizations?user_id=${userId}`);
1214
+
1215
+ // Handle various response formats
1216
+ let authorizations: any[] = [];
1217
+ if (data.data) {
1218
+ authorizations = data.data.authorizations || data.data || [];
1219
+ } else {
1220
+ authorizations = data.authorizations || data || [];
1221
+ }
1222
+
1223
+ set({
1224
+ clientAuthorizations: {
1225
+ ...state.clientAuthorizations,
1226
+ [userId]: Array.isArray(authorizations) ? authorizations : []
1227
+ },
1228
+ isLoadingClientAuthorizations: { ...state.isLoadingClientAuthorizations, [userId]: false }
1229
+ });
1230
+ authLogger.debug(`[AuthStore] Client authorizations fetched for ${userId}`);
1231
+ } catch (error) {
1232
+ const errorMessage = error instanceof Error ? error.message : String(error);
1233
+ authLogger.error(`[AuthStore] Failed to fetch client authorizations for ${userId}:`, error);
1234
+
1235
+ set({
1236
+ clientAuthorizations: {
1237
+ ...state.clientAuthorizations,
1238
+ [userId]: []
1239
+ },
1240
+ isLoadingClientAuthorizations: { ...state.isLoadingClientAuthorizations, [userId]: false }
1241
+ });
1242
+
1243
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1244
+ throw error;
1245
+ }
1246
+ }
1247
+ },
1248
+
1249
+ fetchUserRoleAssignments: async (userId: string, force = false) => {
1250
+ const state = get();
1251
+ const cacheKey = `assignments_${userId}`;
1252
+
1253
+ if (state.isLoadingUserAssignments[userId]) {
1254
+ return;
1255
+ }
1256
+
1257
+ if (!force && state.userAssignments[userId]) {
1258
+ authLogger.debug(`[AuthStore] Using cached role assignments for ${userId}`);
1259
+ return;
1260
+ }
1261
+
1262
+ set({
1263
+ isLoadingUserAssignments: { ...state.isLoadingUserAssignments, [userId]: true }
1264
+ });
1265
+
1266
+ try {
1267
+ const data = await get().apiCall(`/api/admin/users/role-assignments?user_id=${userId}`);
1268
+
1269
+ let assignments: any[] = [];
1270
+ if (data.data) {
1271
+ assignments = data.data.assignments || data.data || [];
1272
+ } else {
1273
+ assignments = data.assignments || data || [];
1274
+ }
1275
+
1276
+ set({
1277
+ userAssignments: {
1278
+ ...state.userAssignments,
1279
+ [userId]: Array.isArray(assignments) ? assignments : []
1280
+ },
1281
+ isLoadingUserAssignments: { ...state.isLoadingUserAssignments, [userId]: false }
1282
+ });
1283
+ authLogger.debug(`[AuthStore] Role assignments fetched for ${userId}`);
1284
+ } catch (error) {
1285
+ const errorMessage = error instanceof Error ? error.message : String(error);
1286
+ authLogger.error(`[AuthStore] Failed to fetch role assignments for ${userId}:`, error);
1287
+
1288
+ set({
1289
+ userAssignments: {
1290
+ ...state.userAssignments,
1291
+ [userId]: []
1292
+ },
1293
+ isLoadingUserAssignments: { ...state.isLoadingUserAssignments, [userId]: false }
1294
+ });
1295
+
1296
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1297
+ throw error;
1298
+ }
1299
+ }
1300
+ },
1301
+
1302
+ fetchRoleCategories: async (force = false) => {
1303
+ const state = get();
1304
+ const now = Date.now();
1305
+
1306
+ // Check cache unless forced (role categories are fairly stable)
1307
+ if (!force && state.roleCategories && state.roleCategoriesLastFetch) {
1308
+ const age = now - state.roleCategoriesLastFetch;
1309
+ if (age < CACHE_DURATION.ROLE_CATEGORIES) {
1310
+ authLogger.debug('[AuthStore] Using cached role categories');
1311
+ return;
1312
+ }
1313
+ }
1314
+
1315
+ set({ isLoadingRoleCategories: true });
1316
+
1317
+ try {
1318
+ const data = await get().apiCall('/api/admin/roles/categories');
1319
+
1320
+ set({
1321
+ roleCategories: Array.isArray(data) ? data : (data.data || []),
1322
+ roleCategoriesLastFetch: now,
1323
+ isLoadingRoleCategories: false,
1324
+ });
1325
+ authLogger.debug('[AuthStore] Role categories fetched successfully');
1326
+ } catch (error) {
1327
+ const errorMessage = error instanceof Error ? error.message : String(error);
1328
+ authLogger.error('[AuthStore] Failed to fetch role categories:', error);
1329
+
1330
+ set({ isLoadingRoleCategories: false });
1331
+
1332
+ if (!errorMessage.includes('Session expired') && !errorMessage.includes('Authentication failed')) {
1333
+ throw error;
1334
+ }
1335
+ }
1336
+ },
1337
+
1338
+ // ===============================
1339
+ // CRUD OPERATIONS
1340
+ // ===============================
1341
+
1342
+ createUser: async (userData: any) => {
1343
+ try {
1344
+ const result = await get().apiCall('/api/admin/users', {
1345
+ method: 'POST',
1346
+ body: JSON.stringify(userData)
1347
+ });
1348
+
1349
+ // Invalidate users cache to force refresh
1350
+ set({ usersLastFetch: null });
1351
+ authLogger.info('[AuthStore] User created successfully');
1352
+ return result;
1353
+ } catch (error) {
1354
+ authLogger.error('[AuthStore] Failed to create user:', error);
1355
+ throw error;
1356
+ }
1357
+ },
1358
+
1359
+ updateUser: async (userId: string, updates: any) => {
1360
+ try {
1361
+ const result = await get().apiCall(`/api/admin/users/${userId}`, {
1362
+ method: 'PUT',
1363
+ body: JSON.stringify(updates)
1364
+ });
1365
+
1366
+ // Invalidate relevant caches
1367
+ const state = get();
1368
+ set({
1369
+ usersLastFetch: null,
1370
+ userDetails: {
1371
+ ...state.userDetails,
1372
+ [userId]: undefined // Force refresh of this user's details
1373
+ }
1374
+ });
1375
+
1376
+ authLogger.info(`[AuthStore] User ${userId} updated successfully`);
1377
+ return result;
1378
+ } catch (error) {
1379
+ authLogger.error(`[AuthStore] Failed to update user ${userId}:`, error);
1380
+ throw error;
1381
+ }
1382
+ },
1383
+
1384
+ deleteUser: async (userId: string) => {
1385
+ try {
1386
+ await get().apiCall(`/api/admin/users/${userId}`, {
1387
+ method: 'DELETE'
1388
+ });
1389
+
1390
+ // Clean up all related data for this user
1391
+ const state = get();
1392
+ const newUserDetails = { ...state.userDetails };
1393
+ const newUserAssignments = { ...state.userAssignments };
1394
+ const newClientAuthorizations = { ...state.clientAuthorizations };
1395
+
1396
+ delete newUserDetails[userId];
1397
+ delete newUserAssignments[userId];
1398
+ delete newClientAuthorizations[userId];
1399
+
1400
+ set({
1401
+ usersLastFetch: null, // Force refresh
1402
+ userDetails: newUserDetails,
1403
+ userAssignments: newUserAssignments,
1404
+ clientAuthorizations: newClientAuthorizations
1405
+ });
1406
+
1407
+ authLogger.info(`[AuthStore] User ${userId} deleted successfully`);
1408
+ } catch (error) {
1409
+ authLogger.error(`[AuthStore] Failed to delete user ${userId}:`, error);
1410
+ throw error;
1411
+ }
1412
+ },
1413
+
1414
+ createRole: async (roleData: any) => {
1415
+ try {
1416
+ const result = await get().apiCall('/api/admin/roles', {
1417
+ method: 'POST',
1418
+ body: JSON.stringify(roleData)
1419
+ });
1420
+
1421
+ // Invalidate roles cache
1422
+ set({ rolesLastFetch: null });
1423
+ authLogger.info('[AuthStore] Role created successfully');
1424
+ return result;
1425
+ } catch (error) {
1426
+ authLogger.error('[AuthStore] Failed to create role:', error);
1427
+ throw error;
1428
+ }
1429
+ },
1430
+
1431
+ updateRole: async (roleId: string, updates: any) => {
1432
+ try {
1433
+ const result = await get().apiCall(`/api/admin/roles/${roleId}`, {
1434
+ method: 'PUT',
1435
+ body: JSON.stringify(updates)
1436
+ });
1437
+
1438
+ set({ rolesLastFetch: null });
1439
+ authLogger.info(`[AuthStore] Role ${roleId} updated successfully`);
1440
+ return result;
1441
+ } catch (error) {
1442
+ authLogger.error(`[AuthStore] Failed to update role ${roleId}:`, error);
1443
+ throw error;
1444
+ }
1445
+ },
1446
+
1447
+ deleteRole: async (roleId: string) => {
1448
+ try {
1449
+ await get().apiCall(`/api/admin/roles/${roleId}`, {
1450
+ method: 'DELETE'
1451
+ });
1452
+
1453
+ set({ rolesLastFetch: null });
1454
+ authLogger.info(`[AuthStore] Role ${roleId} deleted successfully`);
1455
+ } catch (error) {
1456
+ authLogger.error(`[AuthStore] Failed to delete role ${roleId}:`, error);
1457
+ throw error;
1458
+ }
1459
+ },
1460
+
1461
+ assignUserToRole: async (userId: string, roleId: string) => {
1462
+ try {
1463
+ await get().apiCall('/api/admin/users/assign-role', {
1464
+ method: 'POST',
1465
+ body: JSON.stringify({ userId, roleId })
1466
+ });
1467
+
1468
+ // Invalidate user assignments cache
1469
+ const state = get();
1470
+ set({
1471
+ userAssignments: {
1472
+ ...state.userAssignments,
1473
+ [userId]: undefined // Force refresh
1474
+ },
1475
+ usersLastFetch: null // Also refresh users list
1476
+ });
1477
+
1478
+ authLogger.info(`[AuthStore] User ${userId} assigned to role ${roleId}`);
1479
+ } catch (error) {
1480
+ authLogger.error(`[AuthStore] Failed to assign user ${userId} to role ${roleId}:`, error);
1481
+ throw error;
1482
+ }
1483
+ },
1484
+
1485
+ removeUserFromRole: async (userId: string, roleId: string) => {
1486
+ try {
1487
+ await get().apiCall('/api/admin/users/remove-role', {
1488
+ method: 'POST',
1489
+ body: JSON.stringify({ userId, roleId })
1490
+ });
1491
+
1492
+ // Invalidate user assignments cache
1493
+ const state = get();
1494
+ set({
1495
+ userAssignments: {
1496
+ ...state.userAssignments,
1497
+ [userId]: undefined // Force refresh
1498
+ },
1499
+ usersLastFetch: null // Also refresh users list
1500
+ });
1501
+
1502
+ authLogger.info(`[AuthStore] User ${userId} removed from role ${roleId}`);
1503
+ } catch (error) {
1504
+ authLogger.error(`[AuthStore] Failed to remove user ${userId} from role ${roleId}:`, error);
1505
+ throw error;
1506
+ }
1507
+ },
1508
+
1509
+ assignUserToClient: async (userId: string, clientId: string) => {
1510
+ try {
1511
+ await get().apiCall('/api/admin/users/assign-client', {
1512
+ method: 'POST',
1513
+ body: JSON.stringify({ userId, clientId })
1514
+ });
1515
+
1516
+ // Invalidate user client authorizations cache
1517
+ const state = get();
1518
+ set({
1519
+ clientAuthorizations: {
1520
+ ...state.clientAuthorizations,
1521
+ [userId]: undefined // Force refresh
1522
+ },
1523
+ usersLastFetch: null
1524
+ });
1525
+
1526
+ authLogger.info(`[AuthStore] User ${userId} assigned to client ${clientId}`);
1527
+ } catch (error) {
1528
+ authLogger.error(`[AuthStore] Failed to assign user ${userId} to client ${clientId}:`, error);
1529
+ throw error;
1530
+ }
1531
+ },
1532
+
1533
+ removeUserFromClient: async (userId: string, clientId: string) => {
1534
+ try {
1535
+ await get().apiCall('/api/admin/users/remove-client', {
1536
+ method: 'POST',
1537
+ body: JSON.stringify({ userId, clientId })
1538
+ });
1539
+
1540
+ // Invalidate user client authorizations cache
1541
+ const state = get();
1542
+ set({
1543
+ clientAuthorizations: {
1544
+ ...state.clientAuthorizations,
1545
+ [userId]: undefined // Force refresh
1546
+ },
1547
+ usersLastFetch: null
1548
+ });
1549
+
1550
+ authLogger.info(`[AuthStore] User ${userId} removed from client ${clientId}`);
1551
+ } catch (error) {
1552
+ authLogger.error(`[AuthStore] Failed to remove user ${userId} from client ${clientId}:`, error);
1553
+ throw error;
1554
+ }
1555
+ },
1556
+
1557
+ // ===============================
1558
+ // UTILITY ACTIONS
1559
+ // ===============================
1560
+
1561
+ hasRole: (role: string) => {
1562
+ const state = get();
1563
+ return state.user?.roles?.includes(role) || false;
1564
+ },
1565
+
1566
+ hasAnyRole: (roles: string[]) => {
1567
+ const state = get();
1568
+ if (!state.user?.roles) return false;
1569
+ return roles.some(role => state.user!.roles.includes(role));
1570
+ },
1571
+
1572
+ hasAllRoles: (roles: string[]) => {
1573
+ const state = get();
1574
+ if (!state.user?.roles) return false;
1575
+ return roles.every(role => state.user!.roles.includes(role));
1576
+ },
1577
+
1578
+ isFullyAuthenticated: () => {
1579
+ const state = get();
1580
+ return state.isAuthenticated &&
1581
+ (!state.user?.requiresTwoFactor || state.user.twoFactorSessionVerified);
1582
+ },
1583
+
1584
+ // ===============================
1585
+ // ERROR MANAGEMENT
1586
+ // ===============================
1587
+
1588
+ setError: (error: string | null) => {
1589
+ set({ error });
1590
+ },
1591
+
1592
+ clearError: () => {
1593
+ set({ error: null, tokenError: null });
1594
+ },
1595
+
1596
+ // ===============================
1597
+ // ADMIN USER STATE MANAGEMENT ACTIONS
1598
+ // ===============================
1599
+
1600
+ approveUser: async (userId: string, reason?: string) => {
1601
+ try {
1602
+ await get().apiCall('/api/Admin/users/toggle-approval', {
1603
+ method: 'POST',
1604
+ body: JSON.stringify({
1605
+ user_id: parseInt(userId),
1606
+ is_approved: true,
1607
+ reason: reason || 'Administrative approval'
1608
+ })
1609
+ });
1610
+
1611
+ authLogger.info('[AuthStore] User approved:', { userId, reason });
1612
+
1613
+ // The backend should broadcast the SignalR event, but we could also
1614
+ // optimistically update local state here if needed
1615
+ } catch (error) {
1616
+ authLogger.error('[AuthStore] Failed to approve user:', error);
1617
+ throw error;
1618
+ }
1619
+ },
1620
+
1621
+ disapproveUser: async (userId: string, reason?: string) => {
1622
+ try {
1623
+ await get().apiCall('/api/Admin/users/toggle-approval', {
1624
+ method: 'POST',
1625
+ body: JSON.stringify({
1626
+ user_id: parseInt(userId),
1627
+ is_approved: false,
1628
+ reason: reason || 'Administrative disapproval'
1629
+ })
1630
+ });
1631
+ authLogger.info('[AuthStore] User disapproved:', { userId, reason });
1632
+ } catch (error) {
1633
+ authLogger.error('[AuthStore] Failed to disapprove user:', error);
1634
+ throw error;
1635
+ }
1636
+ },
1637
+
1638
+ pauseUser: async (userId: string, reason?: string) => {
1639
+ try {
1640
+ await get().apiCall('/api/Admin/users/pause', {
1641
+ method: 'POST',
1642
+ body: JSON.stringify({
1643
+ user_id: parseInt(userId),
1644
+ reason: reason || 'Administrative suspension'
1645
+ })
1646
+ });
1647
+ authLogger.info('[AuthStore] User paused:', { userId, reason });
1648
+ } catch (error) {
1649
+ authLogger.error('[AuthStore] Failed to pause user:', error);
1650
+ throw error;
1651
+ }
1652
+ },
1653
+
1654
+ resumeUser: async (userId: string) => {
1655
+ try {
1656
+ await get().apiCall('/api/Admin/users/resume', {
1657
+ method: 'POST',
1658
+ body: JSON.stringify({
1659
+ user_id: parseInt(userId)
1660
+ })
1661
+ });
1662
+ authLogger.info('[AuthStore] User resumed:', { userId });
1663
+ } catch (error) {
1664
+ authLogger.error('[AuthStore] Failed to resume user:', error);
1665
+ throw error;
1666
+ }
1667
+ },
1668
+
1669
+ haltUser: async (userId: string, reason?: string) => {
1670
+ try {
1671
+ await get().apiCall('/api/Admin/users/halt', {
1672
+ method: 'POST',
1673
+ body: JSON.stringify({
1674
+ user_id: parseInt(userId),
1675
+ reason: reason || 'Security lockout'
1676
+ })
1677
+ });
1678
+ authLogger.info('[AuthStore] User halted:', { userId, reason });
1679
+ } catch (error) {
1680
+ authLogger.error('[AuthStore] Failed to halt user:', error);
1681
+ throw error;
1682
+ }
1683
+ },
1684
+
1685
+ unlockUser: async (userId: string) => {
1686
+ try {
1687
+ await get().apiCall('/api/Admin/users/unlock', {
1688
+ method: 'POST',
1689
+ body: JSON.stringify({
1690
+ user_id: parseInt(userId)
1691
+ })
1692
+ });
1693
+ authLogger.info('[AuthStore] User unlocked:', { userId });
1694
+ } catch (error) {
1695
+ authLogger.error('[AuthStore] Failed to unlock user:', error);
1696
+ throw error;
1697
+ }
1698
+ },
1699
+
1700
+ // ===============================
1701
+ // USER STATE UTILITY FUNCTIONS
1702
+ // ===============================
1703
+
1704
+ canUserAccess: () => {
1705
+ const state = get();
1706
+ if (!state.user) return false;
1707
+
1708
+ // State priority: LOCKOUT > SUSPENSION > APPROVAL
1709
+ if (state.user.lockoutEnabled) {
1710
+ // Check if lockout has expired
1711
+ if (state.user.lockoutEnd && new Date() > state.user.lockoutEnd) {
1712
+ return true; // Lockout expired
1713
+ }
1714
+ return false; // Still locked
1715
+ }
1716
+
1717
+ if (state.user.isSuspended) {
1718
+ return false; // Suspended
1719
+ }
1720
+
1721
+ return state.user.isApproved; // Must be approved
1722
+ },
1723
+
1724
+ getUserStateDisplay: () => {
1725
+ const state = get();
1726
+ if (!state.user) return 'Unknown';
1727
+
1728
+ // State priority: LOCKOUT > SUSPENSION > APPROVAL
1729
+ if (state.user.lockoutEnabled) {
1730
+ if (state.user.lockoutEnd && new Date() > state.user.lockoutEnd) {
1731
+ return 'Lockout Expired';
1732
+ }
1733
+ return 'Locked Out';
1734
+ }
1735
+
1736
+ if (state.user.isSuspended) {
1737
+ return 'Suspended';
1738
+ }
1739
+
1740
+ return state.user.isApproved ? 'Approved' : 'Unapproved';
1741
+ },
1742
+
1743
+ isUserLocked: () => {
1744
+ const state = get();
1745
+ if (!state.user) return false;
1746
+
1747
+ if (!state.user.lockoutEnabled) return false;
1748
+
1749
+ // Check if lockout has expired
1750
+ if (state.user.lockoutEnd && new Date() > state.user.lockoutEnd) {
1751
+ return false; // Lockout expired
1752
+ }
1753
+
1754
+ return true; // Still locked
1755
+ },
1756
+
1757
+ // ===============================
1758
+ // REAL-TIME SIGNALR MANAGEMENT
1759
+ // ===============================
1760
+
1761
+ initializeSignalR: async () => {
1762
+ const state = get();
1763
+
1764
+ // Don't initialize if already connected or not authenticated
1765
+ if (state.signalrConnection || !state.isAuthenticated || !state.accessToken) {
1766
+ authLogger.debug('[AuthStore] Skipping SignalR - already connected or not authenticated');
1767
+ return;
1768
+ }
1769
+
1770
+ // Check if user has admin role for ActivityHub access
1771
+ const hasAdminRole = state.user?.roles?.includes('payez_admin') || false;
1772
+ if (!hasAdminRole) {
1773
+ authLogger.info('[AuthStore] Skipping SignalR - user lacks admin role for ActivityHub');
1774
+ return;
1775
+ }
1776
+
1777
+ authLogger.info('[AuthStore] Initializing SignalR connection');
1778
+
1779
+ try {
1780
+ // Use the existing signalRActivityService instead of creating a duplicate connection
1781
+ // to the same health hub - this avoids connection errors flooding the console
1782
+
1783
+ // Start the health service if not already running (with timeout)
1784
+ const idpBaseUrl = process.env.IDP_URL;
1785
+ if (!idpBaseUrl) {
1786
+ throw new Error('[IDP_URL] FATAL: IDP_URL environment variable is REQUIRED.');
1787
+ }
1788
+ const startPromise = signalRActivityService.start(idpBaseUrl);
1789
+ const timeoutPromise = new Promise((_, reject) =>
1790
+ setTimeout(() => reject(new Error('SignalR start timeout')), 5000)
1791
+ );
1792
+
1793
+ await Promise.race([startPromise, timeoutPromise]);
1794
+
1795
+ // Subscribe to health status changes from the dedicated health service
1796
+ const unsubscribe = signalRActivityService.subscribe((status) => {
1797
+ // Map health service status to our connection state
1798
+ const connectionState = status.isHealthy ?
1799
+ HubConnectionState.Connected :
1800
+ status.message.includes('reconnecting') ?
1801
+ HubConnectionState.Reconnecting :
1802
+ HubConnectionState.Disconnected;
1803
+
1804
+ authLogger.info('[AuthStore] Health status update:', {
1805
+ isHealthy: status.isHealthy,
1806
+ message: status.message,
1807
+ connectionState
1808
+ });
1809
+
1810
+ set({
1811
+ signalrConnectionState: connectionState,
1812
+ isConnectedToSignalR: status.isHealthy
1813
+ });
1814
+ });
1815
+
1816
+ // Store the unsubscribe function for cleanup
1817
+ set({
1818
+ signalrConnection: {
1819
+ // Minimal connection-like interface that does nothing on stop()
1820
+ // This allows existing code to still call connection.stop() without errors
1821
+ stop: async () => {
1822
+ unsubscribe();
1823
+ authLogger.info('[AuthStore] Unsubscribed from health service');
1824
+ }
1825
+ } as any,
1826
+ signalrConnectionState: signalRActivityService.getCurrentStatus().isHealthy ?
1827
+ HubConnectionState.Connected : HubConnectionState.Disconnected,
1828
+ isConnectedToSignalR: signalRActivityService.getCurrentStatus().isHealthy
1829
+ });
1830
+
1831
+ authLogger.info('[AuthStore] SignalR connection established via health service');
1832
+
1833
+ } catch (error) {
1834
+ // Don't let SignalR errors propagate up and potentially break login flow
1835
+ const errorMessage = error instanceof Error ? error.message : String(error);
1836
+
1837
+ // Only log as error if it's not a timeout or expected connection failure
1838
+ if (errorMessage.includes('timeout') || errorMessage.includes('unavailable')) {
1839
+ authLogger.info('[AuthStore] SignalR connection not available (expected in some environments):', errorMessage);
1840
+ } else {
1841
+ authLogger.error('[AuthStore] SignalR connection failed:', error);
1842
+ }
1843
+
1844
+ set({
1845
+ signalrConnectionState: HubConnectionState.Disconnected,
1846
+ isConnectedToSignalR: false
1847
+ });
1848
+ }
1849
+ },
1850
+
1851
+ disconnectSignalR: async () => {
1852
+ const state = get();
1853
+
1854
+ if (state.signalrConnection) {
1855
+ authLogger.info('[AuthStore] Disconnecting SignalR');
1856
+
1857
+ try {
1858
+ await state.signalrConnection.stop();
1859
+ } catch (error) {
1860
+ authLogger.error('[AuthStore] Error disconnecting SignalR:', error);
1861
+ }
1862
+
1863
+ set({
1864
+ signalrConnection: null,
1865
+ signalrConnectionState: HubConnectionState.Disconnected,
1866
+ isConnectedToSignalR: false
1867
+ });
1868
+ }
1869
+ },
1870
+
1871
+ handleUserStateChanged: (data: UserStateChangeEvent) => {
1872
+ // This method was designed for UserStateChanged events that don't exist in current backend
1873
+ // Keeping for future reference or if we implement proper user state change events
1874
+ const state = get();
1875
+
1876
+ // If this event is about the current user, update their state
1877
+ if (data.userId === state.user?.id && state.session) {
1878
+ authLogger.info('[AuthStore] Updating current user state from SignalR:', data);
1879
+ // ... user state update logic would go here
1880
+ }
1881
+ },
1882
+ }),
1883
+ {
1884
+ name: 'auth-store',
1885
+ }
1886
+ )
1887
+ );
1888
+
1889
+ // ===============================
1890
+ // STORE INITIALIZATION HELPER
1891
+ // ===============================
1892
+
1893
+ /**
1894
+ * Initialize the auth store with a session (typically called in layout)
1895
+ */
1896
+ export const initializeAuthStore = (session: AppSession | null) => {
1897
+ const store = useAuthStore.getState();
1898
+ if (!store.isInitialized) {
1899
+ authLogger.debug('[AuthStore] Initializing with session:', { hasSession: !!session });
1900
+ store.setSession(session);
1901
+ }
1902
+ };
1903
+
1904
+ export default useAuthStore;