@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.
- package/README.md +782 -0
- package/dist/api/auth-handler.d.ts +67 -0
- package/dist/api/auth-handler.js +397 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +19 -0
- package/dist/api-handlers/account/change-password.d.ts +9 -0
- package/dist/api-handlers/account/change-password.js +112 -0
- package/dist/api-handlers/account/masked-info.d.ts +2 -0
- package/dist/api-handlers/account/masked-info.js +41 -0
- package/dist/api-handlers/account/profile.d.ts +3 -0
- package/dist/api-handlers/account/profile.js +63 -0
- package/dist/api-handlers/account/recovery/initiate.d.ts +2 -0
- package/dist/api-handlers/account/recovery/initiate.js +26 -0
- package/dist/api-handlers/account/recovery/send-code.d.ts +2 -0
- package/dist/api-handlers/account/recovery/send-code.js +28 -0
- package/dist/api-handlers/account/recovery/verify-code.d.ts +2 -0
- package/dist/api-handlers/account/recovery/verify-code.js +28 -0
- package/dist/api-handlers/account/reset-password.d.ts +2 -0
- package/dist/api-handlers/account/reset-password.js +26 -0
- package/dist/api-handlers/account/send-code.d.ts +24 -0
- package/dist/api-handlers/account/send-code.js +60 -0
- package/dist/api-handlers/account/update-phone.d.ts +27 -0
- package/dist/api-handlers/account/update-phone.js +64 -0
- package/dist/api-handlers/account/validate-password.d.ts +17 -0
- package/dist/api-handlers/account/validate-password.js +81 -0
- package/dist/api-handlers/account/verify-email.d.ts +26 -0
- package/dist/api-handlers/account/verify-email.js +106 -0
- package/dist/api-handlers/account/verify-sms.d.ts +26 -0
- package/dist/api-handlers/account/verify-sms.js +106 -0
- package/dist/api-handlers/admin/analytics.d.ts +20 -0
- package/dist/api-handlers/admin/analytics.js +379 -0
- package/dist/api-handlers/admin/audit.d.ts +20 -0
- package/dist/api-handlers/admin/audit.js +214 -0
- package/dist/api-handlers/admin/index.d.ts +21 -0
- package/dist/api-handlers/admin/index.js +41 -0
- package/dist/api-handlers/admin/redis-sessions.d.ts +36 -0
- package/dist/api-handlers/admin/redis-sessions.js +204 -0
- package/dist/api-handlers/admin/sessions.d.ts +21 -0
- package/dist/api-handlers/admin/sessions.js +284 -0
- package/dist/api-handlers/admin/site-logs.d.ts +46 -0
- package/dist/api-handlers/admin/site-logs.js +318 -0
- package/dist/api-handlers/admin/users.d.ts +20 -0
- package/dist/api-handlers/admin/users.js +222 -0
- package/dist/api-handlers/admin/vibe-data.d.ts +80 -0
- package/dist/api-handlers/admin/vibe-data.js +268 -0
- package/dist/api-handlers/anon/preferences.d.ts +37 -0
- package/dist/api-handlers/anon/preferences.js +96 -0
- package/dist/api-handlers/auth/jwks.d.ts +2 -0
- package/dist/api-handlers/auth/jwks.js +24 -0
- package/dist/api-handlers/auth/login.d.ts +42 -0
- package/dist/api-handlers/auth/login.js +178 -0
- package/dist/api-handlers/auth/refresh.d.ts +74 -0
- package/dist/api-handlers/auth/refresh.js +635 -0
- package/dist/api-handlers/auth/signout.d.ts +37 -0
- package/dist/api-handlers/auth/signout.js +187 -0
- package/dist/api-handlers/auth/status.d.ts +8 -0
- package/dist/api-handlers/auth/status.js +26 -0
- package/dist/api-handlers/auth/update-session.d.ts +37 -0
- package/dist/api-handlers/auth/update-session.js +95 -0
- package/dist/api-handlers/auth/validate.d.ts +6 -0
- package/dist/api-handlers/auth/validate.js +43 -0
- package/dist/api-handlers/auth/verify-code.d.ts +43 -0
- package/dist/api-handlers/auth/verify-code.js +94 -0
- package/dist/api-handlers/session/refresh-viability.d.ts +14 -0
- package/dist/api-handlers/session/refresh-viability.js +39 -0
- package/dist/api-handlers/session/viability.d.ts +13 -0
- package/dist/api-handlers/session/viability.js +146 -0
- package/dist/api-handlers/test/force-expire.d.ts +23 -0
- package/dist/api-handlers/test/force-expire.js +65 -0
- package/dist/auth/auth-decision.d.ts +39 -0
- package/dist/auth/auth-decision.js +182 -0
- package/dist/auth/auth-options.d.ts +57 -0
- package/dist/auth/auth-options.js +213 -0
- package/dist/auth/callbacks/index.d.ts +6 -0
- package/dist/auth/callbacks/index.js +12 -0
- package/dist/auth/callbacks/jwt.d.ts +45 -0
- package/dist/auth/callbacks/jwt.js +305 -0
- package/dist/auth/callbacks/session.d.ts +60 -0
- package/dist/auth/callbacks/session.js +170 -0
- package/dist/auth/callbacks/signin.d.ts +23 -0
- package/dist/auth/callbacks/signin.js +44 -0
- package/dist/auth/events/index.d.ts +4 -0
- package/dist/auth/events/index.js +8 -0
- package/dist/auth/events/signout.d.ts +17 -0
- package/dist/auth/events/signout.js +32 -0
- package/dist/auth/providers/credentials.d.ts +32 -0
- package/dist/auth/providers/credentials.js +223 -0
- package/dist/auth/providers/index.d.ts +5 -0
- package/dist/auth/providers/index.js +21 -0
- package/dist/auth/providers/oauth.d.ts +26 -0
- package/dist/auth/providers/oauth.js +105 -0
- package/dist/auth/route-config.d.ts +66 -0
- package/dist/auth/route-config.js +190 -0
- package/dist/auth/types/auth-types.d.ts +417 -0
- package/dist/auth/types/auth-types.js +53 -0
- package/dist/auth/types/index.d.ts +6 -0
- package/dist/auth/types/index.js +22 -0
- package/dist/auth/unauthenticated-routes.d.ts +1 -0
- package/dist/auth/unauthenticated-routes.js +19 -0
- package/dist/auth/utils/idp-client.d.ts +94 -0
- package/dist/auth/utils/idp-client.js +383 -0
- package/dist/auth/utils/index.d.ts +5 -0
- package/dist/auth/utils/index.js +21 -0
- package/dist/auth/utils/token-utils.d.ts +84 -0
- package/dist/auth/utils/token-utils.js +219 -0
- package/dist/client/AuthContext.d.ts +19 -0
- package/dist/client/AuthContext.js +112 -0
- package/dist/client/fetch-with-auth.d.ts +11 -0
- package/dist/client/fetch-with-auth.js +44 -0
- package/dist/client/fetchWithSession.d.ts +3 -0
- package/dist/client/fetchWithSession.js +24 -0
- package/dist/client/index.d.ts +9 -0
- package/dist/client/index.js +20 -0
- package/dist/client/useAnonSession.d.ts +36 -0
- package/dist/client/useAnonSession.js +99 -0
- package/dist/components/SessionSync.d.ts +13 -0
- package/dist/components/SessionSync.js +119 -0
- package/dist/components/SignalRHealthCheck.d.ts +10 -0
- package/dist/components/SignalRHealthCheck.js +97 -0
- package/dist/components/account/UserAvatarMenu.d.ts +20 -0
- package/dist/components/account/UserAvatarMenu.js +80 -0
- package/dist/components/account/index.d.ts +7 -0
- package/dist/components/account/index.js +10 -0
- package/dist/components/admin/AlertSettingsTab.d.ts +48 -0
- package/dist/components/admin/AlertSettingsTab.js +351 -0
- package/dist/components/admin/AnalyticsTab.d.ts +22 -0
- package/dist/components/admin/AnalyticsTab.js +167 -0
- package/dist/components/admin/DataBrowserTab.d.ts +19 -0
- package/dist/components/admin/DataBrowserTab.js +252 -0
- package/dist/components/admin/LoggingSettingsTab.d.ts +73 -0
- package/dist/components/admin/LoggingSettingsTab.js +339 -0
- package/dist/components/admin/SessionsTab.d.ts +37 -0
- package/dist/components/admin/SessionsTab.js +165 -0
- package/dist/components/admin/StatsTab.d.ts +53 -0
- package/dist/components/admin/StatsTab.js +161 -0
- package/dist/components/admin/VibeAdminContext.d.ts +32 -0
- package/dist/components/admin/VibeAdminContext.js +38 -0
- package/dist/components/admin/VibeAdminLayout.d.ts +11 -0
- package/dist/components/admin/VibeAdminLayout.js +69 -0
- package/dist/components/admin/index.d.ts +29 -0
- package/dist/components/admin/index.js +44 -0
- package/dist/components/auth/FederatedAuthSection.d.ts +8 -0
- package/dist/components/auth/FederatedAuthSection.js +45 -0
- package/dist/components/auth/ModeAwareLoginPage.d.ts +10 -0
- package/dist/components/auth/ModeAwareLoginPage.js +42 -0
- package/dist/components/auth/ModeAwareSignupPage.d.ts +9 -0
- package/dist/components/auth/ModeAwareSignupPage.js +78 -0
- package/dist/components/auth/TraditionalAuthSection.d.ts +14 -0
- package/dist/components/auth/TraditionalAuthSection.js +20 -0
- package/dist/components/recovery/CompleteStep.d.ts +5 -0
- package/dist/components/recovery/CompleteStep.js +8 -0
- package/dist/components/recovery/InitiateRecoveryStep.d.ts +8 -0
- package/dist/components/recovery/InitiateRecoveryStep.js +20 -0
- package/dist/components/recovery/SelectMethodStep.d.ts +8 -0
- package/dist/components/recovery/SelectMethodStep.js +8 -0
- package/dist/components/recovery/SetPasswordStep.d.ts +6 -0
- package/dist/components/recovery/SetPasswordStep.js +20 -0
- package/dist/components/recovery/VerifyCodeStep.d.ts +10 -0
- package/dist/components/recovery/VerifyCodeStep.js +24 -0
- package/dist/components/reserved/ReservedRecoveryWarning.d.ts +38 -0
- package/dist/components/reserved/ReservedRecoveryWarning.js +92 -0
- package/dist/components/reserved/ReservedStatusBox.d.ts +30 -0
- package/dist/components/reserved/ReservedStatusBox.js +71 -0
- package/dist/components/ui/BetaBadge.d.ts +29 -0
- package/dist/components/ui/BetaBadge.js +38 -0
- package/dist/components/ui/Footer.d.ts +37 -0
- package/dist/components/ui/Footer.js +41 -0
- package/dist/config/env.d.ts +66 -0
- package/dist/config/env.js +57 -0
- package/dist/config/logger.d.ts +57 -0
- package/dist/config/logger.js +73 -0
- package/dist/config/logging-config.d.ts +30 -0
- package/dist/config/logging-config.js +122 -0
- package/dist/config/unauthenticated-routes.d.ts +17 -0
- package/dist/config/unauthenticated-routes.js +24 -0
- package/dist/config/vibe-log-transport.d.ts +79 -0
- package/dist/config/vibe-log-transport.js +203 -0
- package/dist/edge/internal-api-url.d.ts +53 -0
- package/dist/edge/internal-api-url.js +63 -0
- package/dist/edge/middleware.d.ts +14 -0
- package/dist/edge/middleware.js +32 -0
- package/dist/hooks/useAuth.d.ts +23 -0
- package/dist/hooks/useAuth.js +81 -0
- package/dist/hooks/useAuthSettings.d.ts +59 -0
- package/dist/hooks/useAuthSettings.js +93 -0
- package/dist/hooks/useAvailableProviders.d.ts +45 -0
- package/dist/hooks/useAvailableProviders.js +108 -0
- package/dist/hooks/usePasswordValidation.d.ts +27 -0
- package/dist/hooks/usePasswordValidation.js +102 -0
- package/dist/hooks/useProfile.d.ts +15 -0
- package/dist/hooks/useProfile.js +59 -0
- package/dist/hooks/usePublicAuthSettings.d.ts +56 -0
- package/dist/hooks/usePublicAuthSettings.js +131 -0
- package/dist/hooks/useSessionExpiration.d.ts +57 -0
- package/dist/hooks/useSessionExpiration.js +72 -0
- package/dist/hooks/useViabilitySession.d.ts +75 -0
- package/dist/hooks/useViabilitySession.js +268 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +54 -0
- package/dist/lib/anon-session.d.ts +74 -0
- package/dist/lib/anon-session.js +169 -0
- package/dist/lib/api-handler.d.ts +123 -0
- package/dist/lib/api-handler.js +478 -0
- package/dist/lib/app-slug.d.ts +95 -0
- package/dist/lib/app-slug.js +172 -0
- package/dist/lib/demo-mode.d.ts +6 -0
- package/dist/lib/demo-mode.js +16 -0
- package/dist/lib/geolocation.d.ts +64 -0
- package/dist/lib/geolocation.js +235 -0
- package/dist/lib/idp-client-config.d.ts +75 -0
- package/dist/lib/idp-client-config.js +351 -0
- package/dist/lib/idp-fetch.d.ts +14 -0
- package/dist/lib/idp-fetch.js +91 -0
- package/dist/lib/internal-api.d.ts +87 -0
- package/dist/lib/internal-api.js +122 -0
- package/dist/lib/jwt-decode-client.d.ts +10 -0
- package/dist/lib/jwt-decode-client.js +46 -0
- package/dist/lib/jwt-decode.d.ts +48 -0
- package/dist/lib/jwt-decode.js +57 -0
- package/dist/lib/nextauth-secret.d.ts +10 -0
- package/dist/lib/nextauth-secret.js +104 -0
- package/dist/lib/rate-limit-service.d.ts +23 -0
- package/dist/lib/rate-limit-service.js +6 -0
- package/dist/lib/redis.d.ts +5 -0
- package/dist/lib/redis.js +28 -0
- package/dist/lib/refresh-token-validator.d.ts +13 -0
- package/dist/lib/refresh-token-validator.js +117 -0
- package/dist/lib/roles.d.ts +145 -0
- package/dist/lib/roles.js +168 -0
- package/dist/lib/secret-validation.d.ts +4 -0
- package/dist/lib/secret-validation.js +14 -0
- package/dist/lib/session-store.d.ts +166 -0
- package/dist/lib/session-store.js +537 -0
- package/dist/lib/session.d.ts +21 -0
- package/dist/lib/session.js +26 -0
- package/dist/lib/site-logger.d.ts +214 -0
- package/dist/lib/site-logger.js +210 -0
- package/dist/lib/standardized-client-api.d.ts +161 -0
- package/dist/lib/standardized-client-api.js +786 -0
- package/dist/lib/startup-init.d.ts +40 -0
- package/dist/lib/startup-init.js +261 -0
- package/dist/lib/test-aware-get-token.d.ts +2 -0
- package/dist/lib/test-aware-get-token.js +81 -0
- package/dist/lib/token-expiry.d.ts +14 -0
- package/dist/lib/token-expiry.js +39 -0
- package/dist/lib/token-lifecycle.d.ts +52 -0
- package/dist/lib/token-lifecycle.js +398 -0
- package/dist/lib/types/api-responses.d.ts +128 -0
- package/dist/lib/types/api-responses.js +171 -0
- package/dist/lib/user-agent-parser.d.ts +50 -0
- package/dist/lib/user-agent-parser.js +220 -0
- package/dist/logging/api/admin-analytics.d.ts +3 -0
- package/dist/logging/api/admin-analytics.js +45 -0
- package/dist/logging/api/audit-log.d.ts +3 -0
- package/dist/logging/api/audit-log.js +52 -0
- package/dist/logging/components/AdminAnalyticsLayout.d.ts +10 -0
- package/dist/logging/components/AdminAnalyticsLayout.js +11 -0
- package/dist/logging/components/AuditLogViewer.d.ts +7 -0
- package/dist/logging/components/AuditLogViewer.js +51 -0
- package/dist/logging/components/ErrorMetricsCard.d.ts +7 -0
- package/dist/logging/components/ErrorMetricsCard.js +16 -0
- package/dist/logging/components/HealthMetricsCard.d.ts +7 -0
- package/dist/logging/components/HealthMetricsCard.js +19 -0
- package/dist/logging/hooks/useAdminAnalytics.d.ts +24 -0
- package/dist/logging/hooks/useAdminAnalytics.js +22 -0
- package/dist/logging/hooks/useAuditLog.d.ts +6 -0
- package/dist/logging/hooks/useAuditLog.js +25 -0
- package/dist/logging/hooks/useErrorMetrics.d.ts +6 -0
- package/dist/logging/hooks/useErrorMetrics.js +38 -0
- package/dist/logging/hooks/useHealthMetrics.d.ts +6 -0
- package/dist/logging/hooks/useHealthMetrics.js +41 -0
- package/dist/logging/index.d.ts +11 -0
- package/dist/logging/index.js +40 -0
- package/dist/logging/types/analytics.d.ts +68 -0
- package/dist/logging/types/analytics.js +3 -0
- package/dist/logging/types/audit.d.ts +29 -0
- package/dist/logging/types/audit.js +2 -0
- package/dist/logging/types/index.d.ts +2 -0
- package/dist/logging/types/index.js +19 -0
- package/dist/middleware/auth-decision.d.ts +33 -0
- package/dist/middleware/auth-decision.js +65 -0
- package/dist/middleware/create-middleware.d.ts +100 -0
- package/dist/middleware/create-middleware.js +445 -0
- package/dist/middleware/rbac-check.d.ts +44 -0
- package/dist/middleware/rbac-check.js +191 -0
- package/dist/middleware/twofa-presets.d.ts +134 -0
- package/dist/middleware/twofa-presets.js +175 -0
- package/dist/models/DecodedAccessToken.d.ts +17 -0
- package/dist/models/DecodedAccessToken.js +2 -0
- package/dist/models/SessionModel.d.ts +122 -0
- package/dist/models/SessionModel.js +136 -0
- package/dist/pages/admin-login/page.d.ts +31 -0
- package/dist/pages/admin-login/page.js +83 -0
- package/dist/pages/admin-roles/RolesAdminPage.d.ts +15 -0
- package/dist/pages/admin-roles/RolesAdminPage.js +78 -0
- package/dist/pages/admin-roles/index.d.ts +8 -0
- package/dist/pages/admin-roles/index.js +15 -0
- package/dist/pages/admin-roles/modals.d.ts +72 -0
- package/dist/pages/admin-roles/modals.js +154 -0
- package/dist/pages/client-admin/ClientSiteAdminPage.d.ts +79 -0
- package/dist/pages/client-admin/ClientSiteAdminPage.js +177 -0
- package/dist/pages/client-admin/index.d.ts +32 -0
- package/dist/pages/client-admin/index.js +37 -0
- package/dist/pages/login/page.d.ts +22 -0
- package/dist/pages/login/page.js +239 -0
- package/dist/pages/profile/EnhancedProfilePage.d.ts +13 -0
- package/dist/pages/profile/EnhancedProfilePage.js +150 -0
- package/dist/pages/profile/index.d.ts +8 -0
- package/dist/pages/profile/index.js +16 -0
- package/dist/pages/profile/page.d.ts +19 -0
- package/dist/pages/profile/page.js +47 -0
- package/dist/pages/profile/profile-patch.d.ts +1 -0
- package/dist/pages/profile/profile-patch.js +281 -0
- package/dist/pages/recovery/page.d.ts +1 -0
- package/dist/pages/recovery/page.js +142 -0
- package/dist/pages/roles/MyRolesPage.d.ts +24 -0
- package/dist/pages/roles/MyRolesPage.js +71 -0
- package/dist/pages/roles/components.d.ts +63 -0
- package/dist/pages/roles/components.js +108 -0
- package/dist/pages/roles/index.d.ts +8 -0
- package/dist/pages/roles/index.js +19 -0
- package/dist/pages/security/EnhancedSecurityPage.d.ts +14 -0
- package/dist/pages/security/EnhancedSecurityPage.js +248 -0
- package/dist/pages/security/index.d.ts +8 -0
- package/dist/pages/security/index.js +16 -0
- package/dist/pages/security/page.d.ts +21 -0
- package/dist/pages/security/page.js +212 -0
- package/dist/pages/security/security-patch.d.ts +1 -0
- package/dist/pages/security/security-patch.js +302 -0
- package/dist/pages/settings/EnhancedSettingsPage.d.ts +46 -0
- package/dist/pages/settings/EnhancedSettingsPage.js +231 -0
- package/dist/pages/settings/index.d.ts +8 -0
- package/dist/pages/settings/index.js +16 -0
- package/dist/pages/settings/page.d.ts +7 -0
- package/dist/pages/settings/page.js +26 -0
- package/dist/pages/showcase/ShowcasePage.d.ts +13 -0
- package/dist/pages/showcase/ShowcasePage.js +140 -0
- package/dist/pages/showcase/index.d.ts +12 -0
- package/dist/pages/showcase/index.js +17 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +14 -0
- package/dist/pages/test-env/EmergencyLogoutPage.js +98 -0
- package/dist/pages/test-env/JwtInspectPage.d.ts +14 -0
- package/dist/pages/test-env/JwtInspectPage.js +114 -0
- package/dist/pages/test-env/RefreshTokenPage.d.ts +15 -0
- package/dist/pages/test-env/RefreshTokenPage.js +91 -0
- package/dist/pages/test-env/TestEnvPage.d.ts +13 -0
- package/dist/pages/test-env/TestEnvPage.js +49 -0
- package/dist/pages/test-env/index.d.ts +24 -0
- package/dist/pages/test-env/index.js +32 -0
- package/dist/pages/verify-code/page.d.ts +30 -0
- package/dist/pages/verify-code/page.js +408 -0
- package/dist/routes/account/index.d.ts +28 -0
- package/dist/routes/account/index.js +71 -0
- package/dist/routes/account/masked-info.d.ts +33 -0
- package/dist/routes/account/masked-info.js +39 -0
- package/dist/routes/account/send-code.d.ts +37 -0
- package/dist/routes/account/send-code.js +42 -0
- package/dist/routes/account/update-phone.d.ts +13 -0
- package/dist/routes/account/update-phone.js +17 -0
- package/dist/routes/account/verify-email.d.ts +38 -0
- package/dist/routes/account/verify-email.js +43 -0
- package/dist/routes/account/verify-sms.d.ts +38 -0
- package/dist/routes/account/verify-sms.js +43 -0
- package/dist/routes/auth/index.d.ts +19 -0
- package/dist/routes/auth/index.js +64 -0
- package/dist/routes/auth/logout.d.ts +31 -0
- package/dist/routes/auth/logout.js +113 -0
- package/dist/routes/auth/nextauth.d.ts +19 -0
- package/dist/routes/auth/nextauth.js +72 -0
- package/dist/routes/auth/refresh.d.ts +30 -0
- package/dist/routes/auth/refresh.js +51 -0
- package/dist/routes/auth/session.d.ts +72 -0
- package/dist/routes/auth/session.js +180 -0
- package/dist/routes/auth/settings.d.ts +25 -0
- package/dist/routes/auth/settings.js +55 -0
- package/dist/routes/auth/viability.d.ts +52 -0
- package/dist/routes/auth/viability.js +201 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.js +54 -0
- package/dist/routes/session/index.d.ts +6 -0
- package/dist/routes/session/index.js +10 -0
- package/dist/routes/session/refresh-viability.d.ts +16 -0
- package/dist/routes/session/refresh-viability.js +20 -0
- package/dist/services/signalrActivityService.d.ts +44 -0
- package/dist/services/signalrActivityService.js +257 -0
- package/dist/stores/authStore.d.ts +154 -0
- package/dist/stores/authStore.js +1531 -0
- package/dist/theme/ThemeProvider.d.ts +14 -0
- package/dist/theme/ThemeProvider.js +28 -0
- package/dist/theme/default.d.ts +8 -0
- package/dist/theme/default.js +33 -0
- package/dist/theme/index.d.ts +15 -0
- package/dist/theme/index.js +25 -0
- package/dist/theme/types.d.ts +56 -0
- package/dist/theme/types.js +8 -0
- package/dist/theme/useTheme.d.ts +60 -0
- package/dist/theme/useTheme.js +63 -0
- package/dist/theme/utils.d.ts +13 -0
- package/dist/theme/utils.js +39 -0
- package/dist/types/api.d.ts +134 -0
- package/dist/types/api.js +44 -0
- package/dist/types/auth.d.ts +19 -0
- package/dist/types/auth.js +2 -0
- package/dist/types/logging.d.ts +42 -0
- package/dist/types/logging.js +2 -0
- package/dist/types/recovery.d.ts +48 -0
- package/dist/types/recovery.js +2 -0
- package/dist/types/security.d.ts +1 -0
- package/dist/types/security.js +2 -0
- package/dist/utils/api.d.ts +85 -0
- package/dist/utils/api.js +287 -0
- package/dist/utils/circuitBreaker.d.ts +43 -0
- package/dist/utils/circuitBreaker.js +91 -0
- package/dist/utils/error-message.d.ts +1 -0
- package/dist/utils/error-message.js +103 -0
- package/dist/utils/layout/reservedSpace.d.ts +59 -0
- package/dist/utils/layout/reservedSpace.js +102 -0
- package/dist/utils/logout.d.ts +14 -0
- package/dist/utils/logout.js +32 -0
- package/dist/vibe/client.d.ts +261 -0
- package/dist/vibe/client.js +445 -0
- package/dist/vibe/errors.d.ts +83 -0
- package/dist/vibe/errors.js +146 -0
- package/dist/vibe/generic.d.ts +234 -0
- package/dist/vibe/generic.js +369 -0
- package/dist/vibe/hooks/index.d.ts +169 -0
- package/dist/vibe/hooks/index.js +252 -0
- package/dist/vibe/index.d.ts +23 -0
- package/dist/vibe/index.js +67 -0
- package/dist/vibe/sessions.d.ts +161 -0
- package/dist/vibe/sessions.js +391 -0
- package/dist/vibe/types.d.ts +353 -0
- package/dist/vibe/types.js +315 -0
- package/package.json +855 -0
- package/scripts/check-internal-url-usage.sh +73 -0
- package/scripts/dev-broker.ps1 +35 -0
- package/scripts/dev-local.ps1 +45 -0
- package/src/api/auth-handler.ts +550 -0
- package/src/api/index.ts +18 -0
- package/src/api-handlers/account/change-password.ts +145 -0
- package/src/api-handlers/account/masked-info.ts +45 -0
- package/src/api-handlers/account/profile.ts +80 -0
- package/src/api-handlers/account/recovery/initiate.ts +23 -0
- package/src/api-handlers/account/recovery/send-code.ts +25 -0
- package/src/api-handlers/account/recovery/verify-code.ts +25 -0
- package/src/api-handlers/account/reset-password.ts +23 -0
- package/src/api-handlers/account/send-code.ts +76 -0
- package/src/api-handlers/account/update-phone.ts +79 -0
- package/src/api-handlers/account/validate-password.ts +118 -0
- package/src/api-handlers/account/verify-email.ts +125 -0
- package/src/api-handlers/account/verify-sms.ts +125 -0
- package/src/api-handlers/admin/analytics.ts +445 -0
- package/src/api-handlers/admin/audit.ts +225 -0
- package/src/api-handlers/admin/index.ts +59 -0
- package/src/api-handlers/admin/redis-sessions.ts +253 -0
- package/src/api-handlers/admin/sessions.ts +320 -0
- package/src/api-handlers/admin/site-logs.ts +367 -0
- package/src/api-handlers/admin/users.ts +244 -0
- package/src/api-handlers/admin/vibe-data.ts +326 -0
- package/src/api-handlers/anon/preferences.ts +123 -0
- package/src/api-handlers/auth/jwks.ts +20 -0
- package/src/api-handlers/auth/login.ts +240 -0
- package/src/api-handlers/auth/refresh.ts +687 -0
- package/src/api-handlers/auth/signout.ts +212 -0
- package/src/api-handlers/auth/status.ts +23 -0
- package/src/api-handlers/auth/update-session.ts +125 -0
- package/src/api-handlers/auth/validate.ts +44 -0
- package/src/api-handlers/auth/verify-code.ts +129 -0
- package/src/api-handlers/session/refresh-viability.ts +36 -0
- package/src/api-handlers/session/viability.ts +166 -0
- package/src/api-handlers/test/force-expire.ts +67 -0
- package/src/auth/auth-decision.ts +230 -0
- package/src/auth/auth-options.ts +237 -0
- package/src/auth/callbacks/index.ts +7 -0
- package/src/auth/callbacks/jwt.ts +382 -0
- package/src/auth/callbacks/session.ts +243 -0
- package/src/auth/callbacks/signin.ts +56 -0
- package/src/auth/events/index.ts +5 -0
- package/src/auth/events/signout.ts +33 -0
- package/src/auth/providers/credentials.ts +256 -0
- package/src/auth/providers/index.ts +6 -0
- package/src/auth/providers/oauth.ts +114 -0
- package/src/auth/route-config.ts +220 -0
- package/src/auth/types/auth-types.ts +555 -0
- package/src/auth/types/index.ts +7 -0
- package/src/auth/unauthenticated-routes.ts +3 -0
- package/src/auth/utils/idp-client.ts +444 -0
- package/src/auth/utils/index.ts +6 -0
- package/src/auth/utils/token-utils.ts +244 -0
- package/src/client/AuthContext.tsx +140 -0
- package/src/client/fetch-with-auth.ts +48 -0
- package/src/client/fetchWithSession.ts +21 -0
- package/src/client/index.ts +13 -0
- package/src/client/useAnonSession.ts +131 -0
- package/src/components/SessionSync.tsx +137 -0
- package/src/components/SignalRHealthCheck.tsx +131 -0
- package/src/components/account/UserAvatarMenu.tsx +217 -0
- package/src/components/account/index.ts +8 -0
- package/src/components/admin/AlertSettingsTab.tsx +728 -0
- package/src/components/admin/AnalyticsTab.tsx +703 -0
- package/src/components/admin/DataBrowserTab.tsx +505 -0
- package/src/components/admin/LoggingSettingsTab.tsx +665 -0
- package/src/components/admin/SessionsTab.tsx +414 -0
- package/src/components/admin/StatsTab.tsx +379 -0
- package/src/components/admin/VibeAdminContext.tsx +87 -0
- package/src/components/admin/VibeAdminLayout.tsx +185 -0
- package/src/components/admin/index.ts +59 -0
- package/src/components/auth/FederatedAuthSection.tsx +95 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -0
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -0
- package/src/components/auth/TraditionalAuthSection.tsx +99 -0
- package/src/components/recovery/CompleteStep.tsx +36 -0
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -0
- package/src/components/recovery/SelectMethodStep.tsx +73 -0
- package/src/components/recovery/SetPasswordStep.tsx +97 -0
- package/src/components/recovery/VerifyCodeStep.tsx +90 -0
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -0
- package/src/components/reserved/ReservedStatusBox.tsx +118 -0
- package/src/components/ui/BetaBadge.tsx +58 -0
- package/src/components/ui/Footer.tsx +93 -0
- package/src/config/env.ts +57 -0
- package/src/config/logger.ts +62 -0
- package/src/config/logging-config.ts +82 -0
- package/src/config/unauthenticated-routes.ts +19 -0
- package/src/config/vibe-log-transport.ts +250 -0
- package/src/edge/internal-api-url.ts +65 -0
- package/src/edge/middleware.ts +42 -0
- package/src/hooks/useAuth.ts +115 -0
- package/src/hooks/useAuthSettings.ts +97 -0
- package/src/hooks/useAvailableProviders.ts +118 -0
- package/src/hooks/usePasswordValidation.ts +127 -0
- package/src/hooks/useProfile.ts +75 -0
- package/src/hooks/usePublicAuthSettings.ts +149 -0
- package/src/hooks/useSessionExpiration.ts +102 -0
- package/src/hooks/useViabilitySession.ts +335 -0
- package/src/index.ts +63 -0
- package/src/lib/anon-session.ts +213 -0
- package/src/lib/api-handler.ts +625 -0
- package/src/lib/app-slug.ts +178 -0
- package/src/lib/demo-mode.ts +13 -0
- package/src/lib/geolocation.ts +265 -0
- package/src/lib/idp-client-config.ts +442 -0
- package/src/lib/idp-fetch.ts +101 -0
- package/src/lib/internal-api.ts +171 -0
- package/src/lib/jwt-decode-client.ts +45 -0
- package/src/lib/jwt-decode.ts +83 -0
- package/src/lib/nextauth-secret.ts +126 -0
- package/src/lib/rate-limit-service.ts +9 -0
- package/src/lib/redis.ts +27 -0
- package/src/lib/refresh-token-validator.ts +64 -0
- package/src/lib/roles.ts +177 -0
- package/src/lib/secret-validation.ts +8 -0
- package/src/lib/session-store.ts +637 -0
- package/src/lib/session.ts +34 -0
- package/src/lib/site-logger.ts +245 -0
- package/src/lib/standardized-client-api.ts +896 -0
- package/src/lib/startup-init.ts +247 -0
- package/src/lib/test-aware-get-token.ts +30 -0
- package/src/lib/token-expiry.ts +40 -0
- package/src/lib/token-lifecycle.ts +477 -0
- package/src/lib/types/api-responses.ts +336 -0
- package/src/lib/user-agent-parser.ts +252 -0
- package/src/logging/api/admin-analytics.ts +51 -0
- package/src/logging/api/audit-log.ts +53 -0
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -0
- package/src/logging/components/AuditLogViewer.tsx +125 -0
- package/src/logging/components/ErrorMetricsCard.tsx +98 -0
- package/src/logging/components/HealthMetricsCard.tsx +70 -0
- package/src/logging/hooks/useAdminAnalytics.ts +22 -0
- package/src/logging/hooks/useAuditLog.ts +24 -0
- package/src/logging/hooks/useErrorMetrics.ts +40 -0
- package/src/logging/hooks/useHealthMetrics.ts +44 -0
- package/src/logging/index.ts +18 -0
- package/src/logging/types/analytics.ts +81 -0
- package/src/logging/types/audit.ts +31 -0
- package/src/logging/types/index.ts +3 -0
- package/src/middleware/auth-decision.ts +43 -0
- package/src/middleware/create-middleware.ts +626 -0
- package/src/middleware/rbac-check.ts +244 -0
- package/src/middleware/twofa-presets.ts +224 -0
- package/src/models/DecodedAccessToken.ts +17 -0
- package/src/models/SessionModel.ts +258 -0
- package/src/pages/admin-login/page.tsx +229 -0
- package/src/pages/admin-roles/RolesAdminPage.tsx +357 -0
- package/src/pages/admin-roles/index.ts +9 -0
- package/src/pages/admin-roles/modals.tsx +469 -0
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +380 -0
- package/src/pages/client-admin/index.ts +33 -0
- package/src/pages/login/page.tsx +463 -0
- package/src/pages/profile/EnhancedProfilePage.tsx +479 -0
- package/src/pages/profile/index.ts +9 -0
- package/src/pages/profile/page.tsx +166 -0
- package/src/pages/recovery/page.tsx +234 -0
- package/src/pages/roles/MyRolesPage.tsx +211 -0
- package/src/pages/roles/components.tsx +294 -0
- package/src/pages/roles/index.ts +17 -0
- package/src/pages/security/EnhancedSecurityPage.tsx +574 -0
- package/src/pages/security/index.ts +9 -0
- package/src/pages/security/page.tsx +507 -0
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -0
- package/src/pages/settings/index.ts +9 -0
- package/src/pages/settings/page.tsx +47 -0
- package/src/pages/showcase/ShowcasePage.tsx +530 -0
- package/src/pages/showcase/index.ts +13 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +179 -0
- package/src/pages/test-env/JwtInspectPage.tsx +418 -0
- package/src/pages/test-env/RefreshTokenPage.tsx +155 -0
- package/src/pages/test-env/TestEnvPage.tsx +116 -0
- package/src/pages/test-env/index.ts +25 -0
- package/src/pages/verify-code/page.tsx +648 -0
- package/src/routes/account/index.ts +32 -0
- package/src/routes/account/masked-info.ts +37 -0
- package/src/routes/account/send-code.ts +40 -0
- package/src/routes/account/update-phone.ts +13 -0
- package/src/routes/account/verify-email.ts +41 -0
- package/src/routes/account/verify-sms.ts +41 -0
- package/src/routes/auth/index.ts +23 -0
- package/src/routes/auth/logout.ts +127 -0
- package/src/routes/auth/nextauth.ts +71 -0
- package/src/routes/auth/refresh.ts +54 -0
- package/src/routes/auth/session.ts +193 -0
- package/src/routes/auth/settings.ts +75 -0
- package/src/routes/auth/viability.ts +220 -0
- package/src/routes/index.ts +18 -0
- package/src/routes/session/index.ts +7 -0
- package/src/routes/session/refresh-viability.ts +17 -0
- package/src/services/signalrActivityService.ts +258 -0
- package/src/stores/authStore.ts +1904 -0
- package/src/templates/instrumentation.ts +41 -0
- package/src/theme/ThemeProvider.tsx +39 -0
- package/src/theme/default.ts +33 -0
- package/src/theme/index.ts +31 -0
- package/src/theme/types.ts +69 -0
- package/src/theme/useTheme.ts +57 -0
- package/src/theme/utils.ts +40 -0
- package/src/types/api.ts +13 -0
- package/src/types/auth.d.ts +15 -0
- package/src/types/auth.ts +22 -0
- package/src/types/logging.ts +11 -0
- package/src/types/next-auth.d.ts +15 -0
- package/src/types/recovery.ts +54 -0
- package/src/types/security.ts +1 -0
- package/src/utils/api.ts +353 -0
- package/src/utils/circuitBreaker.ts +40 -0
- package/src/utils/error-message.ts +108 -0
- package/src/utils/layout/reservedSpace.ts +124 -0
- package/src/utils/logout.ts +30 -0
- package/src/vibe/client.ts +590 -0
- package/src/vibe/errors.ts +185 -0
- package/src/vibe/generic.ts +429 -0
- package/src/vibe/hooks/index.ts +367 -0
- package/src/vibe/index.ts +121 -0
- package/src/vibe/sessions.ts +551 -0
- 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;
|