@payez/next-mvp 4.1.9 → 4.1.11
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 +792 -792
- package/dist/api-handlers/auth/refresh.js +38 -15
- package/dist/auth/better-auth.js +4 -3
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/ensure-fresh-access-token.js +42 -8
- package/dist/lib/idp-fetch.js +25 -9
- package/dist/lib/session-store.d.ts +1 -0
- package/dist/lib/session-store.js +12 -8
- package/dist/lib/token-lifecycle.d.ts +2 -0
- package/dist/lib/token-lifecycle.js +21 -3
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +893 -893
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +705 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +467 -466
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +266 -266
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/ensure-fresh-access-token.ts +369 -335
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +26 -10
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/session-store.ts +4 -1
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/token-lifecycle.ts +30 -3
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
- package/src/pages/security/index.ts +9 -9
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -371,7 +371,7 @@ function createRefreshHandler(config) {
|
|
|
371
371
|
const errorMessage = idpError.message || 'Token refresh failed';
|
|
372
372
|
// CRITICAL: 401 means token is definitively invalid - always discard
|
|
373
373
|
// This prevents redirect loops where viability check keeps returning canRefresh:true
|
|
374
|
-
|
|
374
|
+
let discardToken = refreshResponse.status === 401 || idpError.discard_token === true;
|
|
375
375
|
const retryable = refreshResponse.status !== 401 && idpError.retryable === true;
|
|
376
376
|
const resolution = idpError.resolution || null;
|
|
377
377
|
// ============================================================================
|
|
@@ -408,20 +408,43 @@ function createRefreshHandler(config) {
|
|
|
408
408
|
console.error('[AUTH_REFRESH] Full IDP error response:', JSON.stringify(responseData, null, 2).substring(0, 1000));
|
|
409
409
|
// If IDP signals to discard token, clear it from Redis
|
|
410
410
|
if (discardToken) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
411
|
+
// Race-safe discard (CAS): only clear if the refresh token we sent is STILL
|
|
412
|
+
// the current one. A concurrent refresh may have already rotated it — our 401
|
|
413
|
+
// is then stale, and wiping here would silently log out a healthy session
|
|
414
|
+
// (the rotation-race wipe, QA 6873 site 2). Skip the wipe + drop terminal if changed.
|
|
415
|
+
const postRefresh = await (0, session_store_1.getSession)(sessionToken);
|
|
416
|
+
const tokenUnchanged = !!originalRefreshToken && postRefresh?.idpRefreshToken === originalRefreshToken;
|
|
417
|
+
if (tokenUnchanged) {
|
|
418
|
+
console.error('[AUTH_REFRESH] ⚠️ REFRESH_TOKEN_REMOVAL: IDP signaled discard_token', {
|
|
419
|
+
reason: 'IDP_DISCARD_TOKEN',
|
|
420
|
+
errorCode,
|
|
421
|
+
userId,
|
|
422
|
+
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
423
|
+
hadRefreshToken: !!currentSession.idpRefreshToken,
|
|
424
|
+
stack: new Error().stack
|
|
425
|
+
});
|
|
426
|
+
await (0, session_store_1.updateSession)(sessionToken, {
|
|
427
|
+
idpRefreshToken: '',
|
|
428
|
+
idpRefreshTokenExpires: undefined,
|
|
429
|
+
refreshTokenClearedAt: Date.now(),
|
|
430
|
+
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${errorCode}`
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// Concurrent refresh already rotated the token — our 401 is stale. Return the
|
|
435
|
+
// freshly-rotated token directly so the loser succeeds without a retry
|
|
436
|
+
// (race-fix follow-up #1).
|
|
437
|
+
discardToken = false;
|
|
438
|
+
console.warn('[AUTH_REFRESH] Discard suppressed: refresh token already rotated by a concurrent refresh — returning the fresh token (race-safe).');
|
|
439
|
+
if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
|
|
440
|
+
return server_1.NextResponse.json({
|
|
441
|
+
refreshed: true,
|
|
442
|
+
reason: 'completed_by_concurrent_request',
|
|
443
|
+
accessTokenExpires: postRefresh.idpAccessTokenExpires,
|
|
444
|
+
hasRefreshToken: !!postRefresh.idpRefreshToken,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
425
448
|
}
|
|
426
449
|
return server_1.NextResponse.json({
|
|
427
450
|
error: errorMessage,
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -60,6 +60,7 @@ const magic_link_1 = require("better-auth/plugins/magic-link");
|
|
|
60
60
|
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
61
61
|
const app_slug_1 = require("../lib/app-slug");
|
|
62
62
|
const redis_1 = require("../lib/redis");
|
|
63
|
+
const session_store_1 = require("../lib/session-store");
|
|
63
64
|
/**
|
|
64
65
|
* Build Better Auth social providers from IDP config.
|
|
65
66
|
*/
|
|
@@ -139,7 +140,7 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
|
|
|
139
140
|
await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
|
|
140
141
|
}
|
|
141
142
|
else {
|
|
142
|
-
await redis.setex(`ba:${appSlug}:${key}`,
|
|
143
|
+
await redis.setex(`ba:${appSlug}:${key}`, session_store_1.SESSION_TTL, value);
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
catch { /* Redis unavailable — cookie cache still works */ }
|
|
@@ -359,13 +360,13 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
359
360
|
};
|
|
360
361
|
// Store in BA Redis session (for decodeSession)
|
|
361
362
|
baData.idpTokens = idpTokenData;
|
|
362
|
-
await (0, redis_1.getRedis)().setex(baKey,
|
|
363
|
+
await (0, redis_1.getRedis)().setex(baKey, session_store_1.SESSION_TTL, JSON.stringify(baData));
|
|
363
364
|
// Write to canonical session store so refresh handler and token lifecycle can find the tokens.
|
|
364
365
|
// Key format: {sessionPrefix}{token} — same key that getSession() reads from.
|
|
365
366
|
try {
|
|
366
367
|
const { getSessionPrefix } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
|
|
367
368
|
const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
|
|
368
|
-
await (0, redis_1.getRedis)().setex(canonicalKey,
|
|
369
|
+
await (0, redis_1.getRedis)().setex(canonicalKey, session_store_1.SESSION_TTL, JSON.stringify({
|
|
369
370
|
...idpTokenData,
|
|
370
371
|
oauthProvider: provider,
|
|
371
372
|
}));
|
|
@@ -58,24 +58,24 @@ function MobileNavDrawer({ isOpen, onClose, navItems, customSections, basePath =
|
|
|
58
58
|
const userEmail = session?.user?.email;
|
|
59
59
|
const displaySource = userName || userEmail;
|
|
60
60
|
const userInitial = displaySource?.charAt(0).toUpperCase() || '?';
|
|
61
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `
|
|
62
|
-
fixed inset-0 bg-black/50 backdrop-blur-sm z-40 lg:hidden
|
|
63
|
-
transition-opacity duration-300
|
|
64
|
-
${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
|
|
65
|
-
`, onClick: onClose, "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { role: "dialog", "aria-modal": "true", "aria-label": "Navigation menu", "aria-expanded": isOpen, className: `
|
|
66
|
-
fixed top-0 right-0 bottom-0 w-80 max-w-[85vw]
|
|
67
|
-
bg-white dark:bg-slate-900
|
|
68
|
-
shadow-[-8px_0_32px_rgba(0,0,0,0.15)]
|
|
69
|
-
dark:shadow-[-8px_0_32px_rgba(0,0,0,0.4)]
|
|
70
|
-
z-50 lg:hidden
|
|
71
|
-
overflow-y-auto
|
|
72
|
-
transition-transform duration-300 ease-out
|
|
73
|
-
${isOpen ? 'translate-x-0' : 'translate-x-full'}
|
|
74
|
-
`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between p-4 border-b border-gray-200 dark:border-white/10", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: "Menu" }), (0, jsx_runtime_1.jsx)("button", { onClick: onClose, className: "\n p-2 rounded-xl\n text-gray-400 hover:text-gray-900\n dark:hover:text-white\n hover:bg-gray-100 dark:hover:bg-white/10\n transition-colors\n ", "aria-label": "Close menu", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "h-5 w-5" }) })] }), isAuthenticated && session?.user && ((0, jsx_runtime_1.jsx)("div", { className: "p-4 border-b border-gray-200 dark:border-white/10", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [session.user.image ? ((0, jsx_runtime_1.jsx)(image_1.default, { src: session.user.image, alt: "", width: 48, height: 48, className: "w-12 h-12 rounded-full", unoptimized: true })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-12 h-12 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold text-lg", children: userInitial })), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 min-w-0", children: [userName && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold text-gray-900 dark:text-white truncate", children: userName })), userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-xs text-gray-500 dark:text-slate-400 truncate", children: userEmail }))] })] }) })), (0, jsx_runtime_1.jsx)("div", { className: "p-2", children: navItems.map((item) => ((0, jsx_runtime_1.jsxs)(link_1.default, { href: item.href, onClick: onClose, className: `
|
|
75
|
-
flex items-center gap-3 px-4 py-3.5 rounded-xl
|
|
76
|
-
transition-colors duration-200
|
|
61
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: `
|
|
62
|
+
fixed inset-0 bg-black/50 backdrop-blur-sm z-40 lg:hidden
|
|
63
|
+
transition-opacity duration-300
|
|
64
|
+
${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
|
|
65
|
+
`, onClick: onClose, "aria-hidden": "true" }), (0, jsx_runtime_1.jsxs)("div", { role: "dialog", "aria-modal": "true", "aria-label": "Navigation menu", "aria-expanded": isOpen, className: `
|
|
66
|
+
fixed top-0 right-0 bottom-0 w-80 max-w-[85vw]
|
|
67
|
+
bg-white dark:bg-slate-900
|
|
68
|
+
shadow-[-8px_0_32px_rgba(0,0,0,0.15)]
|
|
69
|
+
dark:shadow-[-8px_0_32px_rgba(0,0,0,0.4)]
|
|
70
|
+
z-50 lg:hidden
|
|
71
|
+
overflow-y-auto
|
|
72
|
+
transition-transform duration-300 ease-out
|
|
73
|
+
${isOpen ? 'translate-x-0' : 'translate-x-full'}
|
|
74
|
+
`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between p-4 border-b border-gray-200 dark:border-white/10", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: "Menu" }), (0, jsx_runtime_1.jsx)("button", { onClick: onClose, className: "\r\n p-2 rounded-xl\r\n text-gray-400 hover:text-gray-900\r\n dark:hover:text-white\r\n hover:bg-gray-100 dark:hover:bg-white/10\r\n transition-colors\r\n ", "aria-label": "Close menu", children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { className: "h-5 w-5" }) })] }), isAuthenticated && session?.user && ((0, jsx_runtime_1.jsx)("div", { className: "p-4 border-b border-gray-200 dark:border-white/10", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [session.user.image ? ((0, jsx_runtime_1.jsx)(image_1.default, { src: session.user.image, alt: "", width: 48, height: 48, className: "w-12 h-12 rounded-full", unoptimized: true })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-12 h-12 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold text-lg", children: userInitial })), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1 min-w-0", children: [userName && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm font-semibold text-gray-900 dark:text-white truncate", children: userName })), userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-xs text-gray-500 dark:text-slate-400 truncate", children: userEmail }))] })] }) })), (0, jsx_runtime_1.jsx)("div", { className: "p-2", children: navItems.map((item) => ((0, jsx_runtime_1.jsxs)(link_1.default, { href: item.href, onClick: onClose, className: `
|
|
75
|
+
flex items-center gap-3 px-4 py-3.5 rounded-xl
|
|
76
|
+
transition-colors duration-200
|
|
77
77
|
${isActiveRoute(item.href)
|
|
78
78
|
? 'bg-blue-500/10 text-blue-500'
|
|
79
|
-
: 'text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-white/10'}
|
|
80
|
-
`, children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "text-xl", children: item.icon }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: item.label }), isActiveRoute(item.href) && ((0, jsx_runtime_1.jsx)("span", { className: "ml-auto w-2 h-2 rounded-full bg-blue-500" }))] }, item.href))) }), customSections?.map((section, sectionIndex) => ((0, jsx_runtime_1.jsxs)("div", { className: "p-2 border-t border-gray-200 dark:border-white/10", children: [section.title && ((0, jsx_runtime_1.jsx)("p", { className: "px-4 py-2 text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wider", children: section.title })), section.items.map((item, itemIndex) => item.href ? ((0, jsx_runtime_1.jsxs)(link_1.default, { href: item.href, onClick: onClose, className: "\n flex items-center gap-3 px-4 py-3 rounded-xl\n text-gray-900 dark:text-white\n hover:bg-gray-100 dark:hover:bg-white/10\n transition-colors\n ", children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "text-xl", children: item.icon }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: item.label })] }, itemIndex)) : ((0, jsx_runtime_1.jsxs)("button", { onClick: () => handleSectionItemClick(item), className: "\n flex items-center gap-3 px-4 py-3 rounded-xl w-full text-left\n text-gray-900 dark:text-white\n hover:bg-gray-100 dark:hover:bg-white/10\n transition-colors\n ", children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "text-xl", children: item.icon }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: item.label })] }, itemIndex)))] }, sectionIndex))), (0, jsx_runtime_1.jsx)("div", { className: "p-4 mt-auto border-t border-gray-200 dark:border-white/10", children: !isAuthenticated ? (unauthActions ?? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: (0, jsx_runtime_1.jsx)("button", { onClick: handleSignIn, className: "\n w-full px-4 py-3 rounded-xl\n text-blue-500 font-semibold\n border border-blue-500/30\n hover:bg-blue-500/10\n transition-colors\n ", children: "Login" }) }))) : (authFooter ?? ((0, jsx_runtime_1.jsx)(link_1.default, { href: basePath, onClick: onClose, className: "\n flex items-center justify-center gap-2\n w-full px-4 py-3 rounded-xl\n text-gray-500 dark:text-slate-400 font-medium\n hover:bg-gray-100 dark:hover:bg-white/10\n transition-colors\n ", children: "Account Settings" }))) })] })] }));
|
|
79
|
+
: 'text-gray-900 dark:text-white hover:bg-gray-100 dark:hover:bg-white/10'}
|
|
80
|
+
`, children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "text-xl", children: item.icon }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: item.label }), isActiveRoute(item.href) && ((0, jsx_runtime_1.jsx)("span", { className: "ml-auto w-2 h-2 rounded-full bg-blue-500" }))] }, item.href))) }), customSections?.map((section, sectionIndex) => ((0, jsx_runtime_1.jsxs)("div", { className: "p-2 border-t border-gray-200 dark:border-white/10", children: [section.title && ((0, jsx_runtime_1.jsx)("p", { className: "px-4 py-2 text-xs font-semibold text-gray-500 dark:text-slate-400 uppercase tracking-wider", children: section.title })), section.items.map((item, itemIndex) => item.href ? ((0, jsx_runtime_1.jsxs)(link_1.default, { href: item.href, onClick: onClose, className: "\r\n flex items-center gap-3 px-4 py-3 rounded-xl\r\n text-gray-900 dark:text-white\r\n hover:bg-gray-100 dark:hover:bg-white/10\r\n transition-colors\r\n ", children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "text-xl", children: item.icon }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: item.label })] }, itemIndex)) : ((0, jsx_runtime_1.jsxs)("button", { onClick: () => handleSectionItemClick(item), className: "\r\n flex items-center gap-3 px-4 py-3 rounded-xl w-full text-left\r\n text-gray-900 dark:text-white\r\n hover:bg-gray-100 dark:hover:bg-white/10\r\n transition-colors\r\n ", children: [item.icon && (0, jsx_runtime_1.jsx)("span", { className: "text-xl", children: item.icon }), (0, jsx_runtime_1.jsx)("span", { className: "font-medium", children: item.label })] }, itemIndex)))] }, sectionIndex))), (0, jsx_runtime_1.jsx)("div", { className: "p-4 mt-auto border-t border-gray-200 dark:border-white/10", children: !isAuthenticated ? (unauthActions ?? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: (0, jsx_runtime_1.jsx)("button", { onClick: handleSignIn, className: "\r\n w-full px-4 py-3 rounded-xl\r\n text-blue-500 font-semibold\r\n border border-blue-500/30\r\n hover:bg-blue-500/10\r\n transition-colors\r\n ", children: "Login" }) }))) : (authFooter ?? ((0, jsx_runtime_1.jsx)(link_1.default, { href: basePath, onClick: onClose, className: "\r\n flex items-center justify-center gap-2\r\n w-full px-4 py-3 rounded-xl\r\n text-gray-500 dark:text-slate-400 font-medium\r\n hover:bg-gray-100 dark:hover:bg-white/10\r\n transition-colors\r\n ", children: "Account Settings" }))) })] })] }));
|
|
81
81
|
}
|
|
@@ -80,7 +80,7 @@ function UserAvatarMenu({ basePath = '', showProfile = true, showSettings = true
|
|
|
80
80
|
router.push(item.href);
|
|
81
81
|
}
|
|
82
82
|
};
|
|
83
|
-
return ((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "relative", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setIsOpen(!isOpen), className: "flex items-center justify-center h-10 w-10 rounded-full overflow-hidden bg-blue-500 text-white font-semibold text-lg hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900", "aria-label": "User menu", "aria-expanded": isOpen, "aria-haspopup": "true", children: session.user.image ? ((0, jsx_runtime_1.jsx)(image_1.default, { src: session.user.image, alt: "", width: 40, height: 40, className: "w-10 h-10 rounded-full object-cover", unoptimized: true })) : (userInitial) }), isOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute right-0 mt-2 w-56 rounded-md shadow-lg z-50\n bg-white dark:bg-slate-900\n border border-gray-200 dark:border-slate-700", role: "menu", "aria-orientation": "vertical", children: [(0, jsx_runtime_1.jsx)("div", { className: "px-4 py-3 border-b border-gray-200 dark:border-slate-700", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [session.user.image ? ((0, jsx_runtime_1.jsx)(image_1.default, { src: session.user.image, alt: "", width: 32, height: 32, className: "w-8 h-8 rounded-full flex-shrink-0", unoptimized: true })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold text-sm flex-shrink-0", children: userInitial })), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [userName && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-700 dark:text-slate-200 truncate", children: userName })), userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-slate-400 truncate", children: userEmail })), !userName && !userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-slate-400", children: "Signed in" }))] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "py-1", children: [showProfile && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.User, { className: "h-4 w-4" }), label: "Profile", onClick: () => handleNavigation(`${basePath}/profile`) })), showSettings && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Settings, { className: "h-4 w-4" }), label: "Settings", onClick: () => handleNavigation(`${basePath}/settings`) })), showSecurity && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Shield, { className: "h-4 w-4" }), label: "Security", onClick: () => handleNavigation(`${basePath}/security`) })), customItems?.map((item, index) => ((0, jsx_runtime_1.jsx)(MenuItem, { icon: item.icon, label: item.label, onClick: () => handleItemClick(item) }, index)))] }), (0, jsx_runtime_1.jsx)("div", { className: "border-t border-gray-200 dark:border-slate-700 py-1", children: (0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "h-4 w-4" }), label: "Sign Out", onClick: handleSignOut, variant: "danger" }) })] }))] }));
|
|
83
|
+
return ((0, jsx_runtime_1.jsxs)("div", { ref: menuRef, className: "relative", children: [(0, jsx_runtime_1.jsx)("button", { onClick: () => setIsOpen(!isOpen), className: "flex items-center justify-center h-10 w-10 rounded-full overflow-hidden bg-blue-500 text-white font-semibold text-lg hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-slate-900", "aria-label": "User menu", "aria-expanded": isOpen, "aria-haspopup": "true", children: session.user.image ? ((0, jsx_runtime_1.jsx)(image_1.default, { src: session.user.image, alt: "", width: 40, height: 40, className: "w-10 h-10 rounded-full object-cover", unoptimized: true })) : (userInitial) }), isOpen && ((0, jsx_runtime_1.jsxs)("div", { className: "absolute right-0 mt-2 w-56 rounded-md shadow-lg z-50\r\n bg-white dark:bg-slate-900\r\n border border-gray-200 dark:border-slate-700", role: "menu", "aria-orientation": "vertical", children: [(0, jsx_runtime_1.jsx)("div", { className: "px-4 py-3 border-b border-gray-200 dark:border-slate-700", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [session.user.image ? ((0, jsx_runtime_1.jsx)(image_1.default, { src: session.user.image, alt: "", width: 32, height: 32, className: "w-8 h-8 rounded-full flex-shrink-0", unoptimized: true })) : ((0, jsx_runtime_1.jsx)("div", { className: "w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold text-sm flex-shrink-0", children: userInitial })), (0, jsx_runtime_1.jsxs)("div", { className: "min-w-0", children: [userName && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-700 dark:text-slate-200 truncate", children: userName })), userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-slate-400 truncate", children: userEmail })), !userName && !userEmail && ((0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-slate-400", children: "Signed in" }))] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "py-1", children: [showProfile && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.User, { className: "h-4 w-4" }), label: "Profile", onClick: () => handleNavigation(`${basePath}/profile`) })), showSettings && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Settings, { className: "h-4 w-4" }), label: "Settings", onClick: () => handleNavigation(`${basePath}/settings`) })), showSecurity && ((0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Shield, { className: "h-4 w-4" }), label: "Security", onClick: () => handleNavigation(`${basePath}/security`) })), customItems?.map((item, index) => ((0, jsx_runtime_1.jsx)(MenuItem, { icon: item.icon, label: item.label, onClick: () => handleItemClick(item) }, index)))] }), (0, jsx_runtime_1.jsx)("div", { className: "border-t border-gray-200 dark:border-slate-700 py-1", children: (0, jsx_runtime_1.jsx)(MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LogOut, { className: "h-4 w-4" }), label: "Sign Out", onClick: handleSignOut, variant: "danger" }) })] }))] }));
|
|
84
84
|
}
|
|
85
85
|
function MenuItem({ icon, label, onClick, variant = 'default' }) {
|
|
86
86
|
const baseClasses = "flex items-center w-full px-4 py-2 text-sm cursor-pointer transition-colors";
|
|
@@ -277,22 +277,22 @@ function AlertSettingsTab({ isDark = true, apiBasePath = '/api/admin/alerts', on
|
|
|
277
277
|
e.preventDefault();
|
|
278
278
|
addRecipient();
|
|
279
279
|
}
|
|
280
|
-
}, placeholder: "Enter email address", className: `
|
|
281
|
-
w-full px-3 py-2 rounded-lg text-sm
|
|
282
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
283
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
284
|
-
${recipientError ? (isDark ? 'border-red-500' : 'border-red-400') : ''}
|
|
285
|
-
` }), recipientError && ((0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-1 ${isDark ? 'text-red-400' : 'text-red-500'}`, children: recipientError }))] }), (0, jsx_runtime_1.jsxs)("button", { onClick: addRecipient, className: `
|
|
286
|
-
px-4 py-2 rounded-lg font-medium text-sm transition-colors
|
|
287
|
-
${isDark ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-indigo-500 hover:bg-indigo-600'}
|
|
288
|
-
text-white flex items-center gap-2
|
|
280
|
+
}, placeholder: "Enter email address", className: `
|
|
281
|
+
w-full px-3 py-2 rounded-lg text-sm
|
|
282
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
283
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
284
|
+
${recipientError ? (isDark ? 'border-red-500' : 'border-red-400') : ''}
|
|
285
|
+
` }), recipientError && ((0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-1 ${isDark ? 'text-red-400' : 'text-red-500'}`, children: recipientError }))] }), (0, jsx_runtime_1.jsxs)("button", { onClick: addRecipient, className: `
|
|
286
|
+
px-4 py-2 rounded-lg font-medium text-sm transition-colors
|
|
287
|
+
${isDark ? 'bg-indigo-600 hover:bg-indigo-700' : 'bg-indigo-500 hover:bg-indigo-600'}
|
|
288
|
+
text-white flex items-center gap-2
|
|
289
289
|
`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 16 }), "Add"] })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsxs)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: isDark ? 'text-cyan-400' : 'text-cyan-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Delivery Mode" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted} mt-1`, children: "How alerts are delivered to recipients" })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-5", children: (0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3", children: DELIVERY_MODES.map((mode) => {
|
|
290
290
|
const isSelected = editedMode === mode.value;
|
|
291
|
-
return ((0, jsx_runtime_1.jsxs)("button", { onClick: () => setEditedMode(mode.value), className: `
|
|
292
|
-
p-4 rounded-lg border-2 text-left transition-all
|
|
291
|
+
return ((0, jsx_runtime_1.jsxs)("button", { onClick: () => setEditedMode(mode.value), className: `
|
|
292
|
+
p-4 rounded-lg border-2 text-left transition-all
|
|
293
293
|
${isSelected
|
|
294
294
|
? 'border-indigo-500 ' + (isDark ? 'bg-indigo-500/10' : 'bg-indigo-50')
|
|
295
|
-
: (isDark ? 'border-slate-700 hover:border-slate-600' : 'border-gray-200 hover:border-gray-300')}
|
|
295
|
+
: (isDark ? 'border-slate-700 hover:border-slate-600' : 'border-gray-200 hover:border-gray-300')}
|
|
296
296
|
`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 mb-1", children: [(0, jsx_runtime_1.jsx)("div", { className: `w-3 h-3 rounded-full border-2 flex items-center justify-center ${isSelected ? 'border-indigo-500' : (isDark ? 'border-slate-500' : 'border-gray-400')}`, children: isSelected && (0, jsx_runtime_1.jsx)("div", { className: "w-1.5 h-1.5 rounded-full bg-indigo-500" }) }), (0, jsx_runtime_1.jsx)("span", { className: `font-medium ${themeClasses.textPrimary}`, children: mode.label })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${themeClasses.textMuted} ml-5`, children: mode.description })] }, mode.value));
|
|
297
297
|
}) }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsxs)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Bell, { className: isDark ? 'text-amber-400' : 'text-amber-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Alert Types" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted} mt-1`, children: "Configure which alerts to receive and their thresholds" })] }), (0, jsx_runtime_1.jsx)("div", { className: "divide-y divide-slate-700/50", children: ALERT_TYPES.map((alertType) => {
|
|
298
298
|
const config = editedAlerts[alertType.id];
|
|
@@ -305,47 +305,47 @@ function AlertSettingsTab({ isDark = true, apiBasePath = '/api/admin/alerts', on
|
|
|
305
305
|
else {
|
|
306
306
|
updateAlertConfig(alertType.id, { threshold: val });
|
|
307
307
|
}
|
|
308
|
-
}, disabled: !config.enabled && !alertType.alwaysSent, className: `
|
|
309
|
-
w-16 px-2 py-1.5 rounded text-sm text-center
|
|
310
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
311
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
312
|
-
disabled:opacity-50
|
|
313
|
-
` }), (0, jsx_runtime_1.jsx)("span", { className: `text-xs ${themeClasses.textMuted} whitespace-nowrap`, children: alertType.thresholdUnit })] })), !alertType.alwaysSent && ((0, jsx_runtime_1.jsx)("button", { onClick: () => updateAlertConfig(alertType.id, { enabled: !config.enabled }), className: `
|
|
314
|
-
relative w-11 h-6 rounded-full transition-colors
|
|
308
|
+
}, disabled: !config.enabled && !alertType.alwaysSent, className: `
|
|
309
|
+
w-16 px-2 py-1.5 rounded text-sm text-center
|
|
310
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
311
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
312
|
+
disabled:opacity-50
|
|
313
|
+
` }), (0, jsx_runtime_1.jsx)("span", { className: `text-xs ${themeClasses.textMuted} whitespace-nowrap`, children: alertType.thresholdUnit })] })), !alertType.alwaysSent && ((0, jsx_runtime_1.jsx)("button", { onClick: () => updateAlertConfig(alertType.id, { enabled: !config.enabled }), className: `
|
|
314
|
+
relative w-11 h-6 rounded-full transition-colors
|
|
315
315
|
${config.enabled
|
|
316
316
|
? (isDark ? 'bg-indigo-600' : 'bg-indigo-500')
|
|
317
|
-
: (isDark ? 'bg-slate-600' : 'bg-gray-300')}
|
|
318
|
-
`, children: (0, jsx_runtime_1.jsx)("span", { className: `
|
|
319
|
-
absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform
|
|
320
|
-
${config.enabled ? 'translate-x-5' : ''}
|
|
317
|
+
: (isDark ? 'bg-slate-600' : 'bg-gray-300')}
|
|
318
|
+
`, children: (0, jsx_runtime_1.jsx)("span", { className: `
|
|
319
|
+
absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform
|
|
320
|
+
${config.enabled ? 'translate-x-5' : ''}
|
|
321
321
|
` }) }))] })] }) }, alertType.id));
|
|
322
|
-
}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsx)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Zap, { className: isDark ? 'text-emerald-400' : 'text-emerald-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Rate Limiting" })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "p-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsx)("label", { className: `text-sm ${themeClasses.textSecondary}`, children: "Maximum alerts:" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: "1", max: "100", value: editedRateLimit, onChange: (e) => setEditedRateLimit(parseInt(e.target.value) || 10), className: `
|
|
323
|
-
w-20 px-3 py-2 rounded-lg text-sm
|
|
324
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
325
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
322
|
+
}) })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsx)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Zap, { className: isDark ? 'text-emerald-400' : 'text-emerald-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Rate Limiting" })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "p-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsx)("label", { className: `text-sm ${themeClasses.textSecondary}`, children: "Maximum alerts:" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: "1", max: "100", value: editedRateLimit, onChange: (e) => setEditedRateLimit(parseInt(e.target.value) || 10), className: `
|
|
323
|
+
w-20 px-3 py-2 rounded-lg text-sm
|
|
324
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
325
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
326
326
|
` }), (0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "per hour" })] }), (0, jsx_runtime_1.jsxs)("div", { className: `flex items-start gap-2 mt-3 text-xs ${themeClasses.textMuted}`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Info, { size: 14, className: "flex-shrink-0 mt-0.5" }), (0, jsx_runtime_1.jsx)("span", { children: "Critical alerts (Storage Critical, Agent Expired) are always sent regardless of this limit." })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsx)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Send, { className: isDark ? 'text-blue-400' : 'text-blue-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Test & History" })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "p-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap gap-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsxs)("select", { disabled: sendingTest || editedRecipients.length === 0, onChange: (e) => {
|
|
327
327
|
if (e.target.value) {
|
|
328
328
|
sendTestAlert(e.target.value);
|
|
329
329
|
e.target.value = '';
|
|
330
330
|
}
|
|
331
|
-
}, className: `
|
|
332
|
-
px-4 py-2.5 pr-10 rounded-lg text-sm font-medium cursor-pointer appearance-none
|
|
333
|
-
${isDark ? 'bg-blue-900/30 text-blue-300 border-blue-700 hover:bg-blue-900/50' : 'bg-blue-50 text-blue-700 border-blue-300 hover:bg-blue-100'}
|
|
334
|
-
border focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
335
|
-
disabled:opacity-50 disabled:cursor-not-allowed
|
|
331
|
+
}, className: `
|
|
332
|
+
px-4 py-2.5 pr-10 rounded-lg text-sm font-medium cursor-pointer appearance-none
|
|
333
|
+
${isDark ? 'bg-blue-900/30 text-blue-300 border-blue-700 hover:bg-blue-900/50' : 'bg-blue-50 text-blue-700 border-blue-300 hover:bg-blue-100'}
|
|
334
|
+
border focus:outline-none focus:ring-2 focus:ring-blue-500
|
|
335
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
336
336
|
`, children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Send Test Alert..." }), TEST_ALERT_OPTIONS.map((opt) => ((0, jsx_runtime_1.jsx)("option", { value: opt.value, children: opt.label }, opt.value)))] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: `absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${isDark ? 'text-blue-400' : 'text-blue-500'}` })] }), (0, jsx_runtime_1.jsxs)("button", { onClick: () => {
|
|
337
337
|
// TODO: Open history modal or navigate to history page
|
|
338
338
|
window.open(`${apiBasePath}/history`, '_blank');
|
|
339
|
-
}, className: `
|
|
340
|
-
px-4 py-2.5 rounded-lg text-sm font-medium flex items-center gap-2
|
|
341
|
-
${isDark ? 'bg-slate-700 text-white hover:bg-slate-600' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}
|
|
342
|
-
border ${isDark ? 'border-slate-600' : 'border-gray-300'}
|
|
343
|
-
`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.History, { size: 16 }), "View Alert History"] })] }), editedRecipients.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-3 ${themeClasses.textMuted}`, children: "Add at least one recipient to send test alerts." }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-end gap-4", children: [hasChanges && ((0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "You have unsaved changes" })), (0, jsx_runtime_1.jsx)("button", { onClick: saveSettings, disabled: !hasChanges || saving, className: `
|
|
344
|
-
px-6 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
339
|
+
}, className: `
|
|
340
|
+
px-4 py-2.5 rounded-lg text-sm font-medium flex items-center gap-2
|
|
341
|
+
${isDark ? 'bg-slate-700 text-white hover:bg-slate-600' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}
|
|
342
|
+
border ${isDark ? 'border-slate-600' : 'border-gray-300'}
|
|
343
|
+
`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.History, { size: 16 }), "View Alert History"] })] }), editedRecipients.length === 0 && ((0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-3 ${themeClasses.textMuted}`, children: "Add at least one recipient to send test alerts." }))] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-end gap-4", children: [hasChanges && ((0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "You have unsaved changes" })), (0, jsx_runtime_1.jsx)("button", { onClick: saveSettings, disabled: !hasChanges || saving, className: `
|
|
344
|
+
px-6 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
345
345
|
${hasChanges
|
|
346
346
|
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
|
347
|
-
: (isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-400')}
|
|
348
|
-
disabled:cursor-not-allowed
|
|
347
|
+
: (isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-400')}
|
|
348
|
+
disabled:cursor-not-allowed
|
|
349
349
|
`, children: saving ? ((0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.RefreshCw, { className: "w-4 h-4 animate-spin" }), "Saving..."] })) : ('Save Settings') })] })] }));
|
|
350
350
|
}
|
|
351
351
|
exports.default = AlertSettingsTab;
|
|
@@ -280,18 +280,18 @@ function LoggingSettingsTab({ isDark = true, apiBasePath = '/api/admin/logging',
|
|
|
280
280
|
}
|
|
281
281
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "space-y-6", children: [error && ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center gap-3 p-4 rounded-lg ${isDark ? 'bg-red-900/30 border-red-700' : 'bg-red-50 border-red-300'} border`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.AlertCircle, { className: isDark ? 'text-red-400' : 'text-red-500', size: 20 }), (0, jsx_runtime_1.jsx)("span", { className: isDark ? 'text-red-300' : 'text-red-700', children: error })] })), successMessage && ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center gap-3 p-4 rounded-lg ${isDark ? 'bg-green-900/30 border-green-700' : 'bg-green-50 border-green-300'} border`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: isDark ? 'text-green-400' : 'text-green-500', size: 20 }), (0, jsx_runtime_1.jsx)("span", { className: isDark ? 'text-green-300' : 'text-green-700', children: successMessage })] })), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsxs)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Zap, { className: isDark ? 'text-amber-400' : 'text-amber-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Quick Setup" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted} mt-1`, children: "Choose a preset to quickly configure log levels" })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-5", children: (0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-3", children: PRESETS.map((preset) => {
|
|
282
282
|
const isActive = getActivePreset() === preset.id;
|
|
283
|
-
return ((0, jsx_runtime_1.jsx)("button", { onClick: () => applyPreset(preset.id), className: `
|
|
284
|
-
px-4 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
283
|
+
return ((0, jsx_runtime_1.jsx)("button", { onClick: () => applyPreset(preset.id), className: `
|
|
284
|
+
px-4 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
285
285
|
${isActive
|
|
286
286
|
? 'bg-indigo-600 text-white ring-2 ring-indigo-500 ring-offset-2 ' + (isDark ? 'ring-offset-slate-800' : 'ring-offset-white')
|
|
287
|
-
: (isDark ? 'bg-slate-700 text-white hover:bg-slate-600' : 'bg-gray-100 text-gray-700 hover:bg-gray-200')}
|
|
287
|
+
: (isDark ? 'bg-slate-700 text-white hover:bg-slate-600' : 'bg-gray-100 text-gray-700 hover:bg-gray-200')}
|
|
288
288
|
`, children: preset.label }, preset.id));
|
|
289
289
|
}) }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsxs)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Bug, { className: isDark ? 'text-violet-400' : 'text-violet-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Log Levels" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted} mt-1`, children: "Set verbosity per category" })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-5", children: (0, jsx_runtime_1.jsx)("div", { className: "space-y-4", children: CATEGORIES.map((category) => ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${themeClasses.textPrimary}`, children: category.label }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${themeClasses.textMuted}`, children: category.description })] }), (0, jsx_runtime_1.jsx)("select", { value: editedLevels?.[category.id] || 'info', onChange: (e) => {
|
|
290
290
|
setEditedLevels(prev => prev ? { ...prev, [category.id]: e.target.value } : null);
|
|
291
|
-
}, className: `
|
|
292
|
-
px-3 py-2 rounded-lg text-sm font-medium cursor-pointer
|
|
293
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
294
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
291
|
+
}, className: `
|
|
292
|
+
px-3 py-2 rounded-lg text-sm font-medium cursor-pointer
|
|
293
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
294
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
295
295
|
`, children: LOG_LEVELS.map((level) => ((0, jsx_runtime_1.jsx)("option", { value: level.value, children: level.label }, level.value))) })] }, category.id))) }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsxs)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Clock, { className: isDark ? 'text-cyan-400' : 'text-cyan-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Retention Settings" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted} mt-1`, children: "How long to keep logs by severity level" })] }), (0, jsx_runtime_1.jsx)("div", { className: "p-5", children: (0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4", children: [
|
|
296
296
|
{ key: 'debug_days', label: 'Debug' },
|
|
297
297
|
{ key: 'info_days', label: 'Info' },
|
|
@@ -300,22 +300,22 @@ function LoggingSettingsTab({ isDark = true, apiBasePath = '/api/admin/logging',
|
|
|
300
300
|
{ key: 'critical_days', label: 'Critical' },
|
|
301
301
|
].map((item) => ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm ${themeClasses.textSecondary} mb-1`, children: item.label }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "number", min: "1", max: "365", value: editedRetention?.[item.key] || 7, onChange: (e) => {
|
|
302
302
|
setEditedRetention(prev => prev ? { ...prev, [item.key]: parseInt(e.target.value) || 7 } : null);
|
|
303
|
-
}, className: `
|
|
304
|
-
w-20 px-3 py-2 rounded-lg text-sm
|
|
305
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
306
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
303
|
+
}, className: `
|
|
304
|
+
w-20 px-3 py-2 rounded-lg text-sm
|
|
305
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
306
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
307
307
|
` }), (0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "days" })] })] }, item.key))) }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: `${themeClasses.cardBg} border rounded-xl overflow-hidden`, children: [(0, jsx_runtime_1.jsx)("div", { className: `px-5 py-4 border-b ${isDark ? 'border-slate-700' : 'border-gray-200'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.HardDrive, { className: isDark ? 'text-emerald-400' : 'text-emerald-500', size: 20 }), (0, jsx_runtime_1.jsx)("h3", { className: `font-semibold ${themeClasses.textPrimary}`, children: "Storage Limits" })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "p-5 space-y-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap items-center gap-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("label", { className: `text-sm ${themeClasses.textSecondary}`, children: "Max size:" }), (0, jsx_runtime_1.jsx)("input", { type: "number", min: "1", max: "1000", value: editedLimits?.max_size_mb || 10, onChange: (e) => {
|
|
308
308
|
setEditedLimits(prev => prev ? { ...prev, max_size_mb: parseInt(e.target.value) || 10 } : null);
|
|
309
|
-
}, className: `
|
|
310
|
-
w-20 px-3 py-2 rounded-lg text-sm
|
|
311
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
312
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
309
|
+
}, className: `
|
|
310
|
+
w-20 px-3 py-2 rounded-lg text-sm
|
|
311
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
312
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
313
313
|
` }), (0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "MB" })] }), (0, jsx_runtime_1.jsx)("span", { className: themeClasses.textMuted, children: "or" }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "number", min: "100", max: "1000000", step: "1000", value: editedLimits?.max_rows || 20000, onChange: (e) => {
|
|
314
314
|
setEditedLimits(prev => prev ? { ...prev, max_rows: parseInt(e.target.value) || 20000 } : null);
|
|
315
|
-
}, className: `
|
|
316
|
-
w-28 px-3 py-2 rounded-lg text-sm
|
|
317
|
-
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
318
|
-
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
315
|
+
}, className: `
|
|
316
|
+
w-28 px-3 py-2 rounded-lg text-sm
|
|
317
|
+
${themeClasses.inputBg} ${themeClasses.textPrimary} border
|
|
318
|
+
focus:outline-none focus:ring-2 focus:ring-indigo-500
|
|
319
319
|
` }), (0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "rows" })] })] }), usage && ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textSecondary}`, children: "Current usage" }), (0, jsx_runtime_1.jsxs)("span", { className: `text-sm font-medium ${themeClasses.textPrimary}`, children: [usage.total_size_mb?.toFixed(1) || 0, " MB of ", usage.limits.max_size_mb, " MB (", usage.percent_used || 0, "%)"] })] }), (0, jsx_runtime_1.jsx)("div", { className: `h-3 rounded-full overflow-hidden ${isDark ? 'bg-slate-700' : 'bg-gray-200'}`, children: (0, jsx_runtime_1.jsx)("div", { className: `h-full rounded-full transition-all ${(usage.percent_used || 0) > 90 ? 'bg-red-500' :
|
|
320
320
|
(usage.percent_used || 0) > 70 ? 'bg-amber-500' :
|
|
321
321
|
'bg-emerald-500'}`, style: { width: `${Math.min(usage.percent_used || 0, 100)}%` } }) }), (0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between mt-2 text-xs ${themeClasses.textMuted}`, children: [(0, jsx_runtime_1.jsxs)("span", { children: [(usage.total_rows || 0).toLocaleString(), " rows"] }), usage.oldest_entry && ((0, jsx_runtime_1.jsxs)("span", { children: ["Oldest: ", new Date(usage.oldest_entry).toLocaleDateString()] }))] })] })), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsxs)("select", { disabled: pruning, onChange: (e) => {
|
|
@@ -323,17 +323,17 @@ function LoggingSettingsTab({ isDark = true, apiBasePath = '/api/admin/logging',
|
|
|
323
323
|
handlePrune(e.target.value);
|
|
324
324
|
e.target.value = '';
|
|
325
325
|
}
|
|
326
|
-
}, className: `
|
|
327
|
-
px-4 py-2.5 pr-10 rounded-lg text-sm font-medium cursor-pointer appearance-none
|
|
328
|
-
${isDark ? 'bg-red-900/30 text-red-300 border-red-700 hover:bg-red-900/50' : 'bg-red-50 text-red-700 border-red-300 hover:bg-red-100'}
|
|
329
|
-
border focus:outline-none focus:ring-2 focus:ring-red-500
|
|
330
|
-
disabled:opacity-50 disabled:cursor-not-allowed
|
|
331
|
-
`, children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Prune Now..." }), PRUNE_OPTIONS.map((opt) => ((0, jsx_runtime_1.jsx)("option", { value: opt.value, children: opt.label }, opt.value)))] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: `absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${isDark ? 'text-red-400' : 'text-red-500'}` })] }), pruning && ((0, jsx_runtime_1.jsxs)("span", { className: `flex items-center gap-2 text-sm ${themeClasses.textMuted}`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.RefreshCw, { className: "w-4 h-4 animate-spin" }), "Pruning..."] }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-end gap-4", children: [hasChanges && ((0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "You have unsaved changes" })), (0, jsx_runtime_1.jsx)("button", { onClick: saveSettings, disabled: !hasChanges || saving, className: `
|
|
332
|
-
px-6 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
326
|
+
}, className: `
|
|
327
|
+
px-4 py-2.5 pr-10 rounded-lg text-sm font-medium cursor-pointer appearance-none
|
|
328
|
+
${isDark ? 'bg-red-900/30 text-red-300 border-red-700 hover:bg-red-900/50' : 'bg-red-50 text-red-700 border-red-300 hover:bg-red-100'}
|
|
329
|
+
border focus:outline-none focus:ring-2 focus:ring-red-500
|
|
330
|
+
disabled:opacity-50 disabled:cursor-not-allowed
|
|
331
|
+
`, children: [(0, jsx_runtime_1.jsx)("option", { value: "", children: "Prune Now..." }), PRUNE_OPTIONS.map((opt) => ((0, jsx_runtime_1.jsx)("option", { value: opt.value, children: opt.label }, opt.value)))] }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: `absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none ${isDark ? 'text-red-400' : 'text-red-500'}` })] }), pruning && ((0, jsx_runtime_1.jsxs)("span", { className: `flex items-center gap-2 text-sm ${themeClasses.textMuted}`, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.RefreshCw, { className: "w-4 h-4 animate-spin" }), "Pruning..."] }))] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-end gap-4", children: [hasChanges && ((0, jsx_runtime_1.jsx)("span", { className: `text-sm ${themeClasses.textMuted}`, children: "You have unsaved changes" })), (0, jsx_runtime_1.jsx)("button", { onClick: saveSettings, disabled: !hasChanges || saving, className: `
|
|
332
|
+
px-6 py-2.5 rounded-lg font-medium text-sm transition-all
|
|
333
333
|
${hasChanges
|
|
334
334
|
? 'bg-indigo-600 text-white hover:bg-indigo-700'
|
|
335
|
-
: (isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-400')}
|
|
336
|
-
disabled:cursor-not-allowed
|
|
335
|
+
: (isDark ? 'bg-slate-700 text-slate-400' : 'bg-gray-200 text-gray-400')}
|
|
336
|
+
disabled:cursor-not-allowed
|
|
337
337
|
`, children: saving ? ((0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.RefreshCw, { className: "w-4 h-4 animate-spin" }), "Saving..."] })) : ('Save Settings') })] })] }));
|
|
338
338
|
}
|
|
339
339
|
exports.default = LoggingSettingsTab;
|
|
@@ -158,8 +158,16 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
158
158
|
if (isOAuthSession && session.mfaVerified && acrValue === '1') {
|
|
159
159
|
acrValue = '2';
|
|
160
160
|
}
|
|
161
|
+
// WO-2: send the POST-lock current refresh token, not the pre-lock snapshot.
|
|
162
|
+
// `session` was read before we acquired the lock; a concurrent refresh that
|
|
163
|
+
// completed while we waited rotates the token, so the pre-lock value is stale
|
|
164
|
+
// and would draw an avoidable 401 (the CAS now catches the resulting wipe, but
|
|
165
|
+
// the round-trip is wasted). `latest` is the authoritative post-lock session.
|
|
166
|
+
// Auth-context (amr/acr/2FA) intentionally still derives from `session` — it is
|
|
167
|
+
// the identity context of THIS request, unchanged by a token rotation.
|
|
168
|
+
const sentRefreshToken = latest?.idpRefreshToken ?? session.idpRefreshToken;
|
|
161
169
|
const body = {
|
|
162
|
-
refresh_token:
|
|
170
|
+
refresh_token: sentRefreshToken,
|
|
163
171
|
amr: authMethods,
|
|
164
172
|
acr: acrValue,
|
|
165
173
|
};
|
|
@@ -200,15 +208,41 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
200
208
|
if (!idpResponse.ok) {
|
|
201
209
|
const idpError = responseData?.error || {};
|
|
202
210
|
const code = idpError.code || 'UNKNOWN_ERROR';
|
|
203
|
-
|
|
211
|
+
let discardToken = idpResponse.status === 401 || idpError.discard_token === true;
|
|
204
212
|
const retryable = idpResponse.status !== 401 && idpError.retryable === true;
|
|
205
213
|
if (discardToken) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
// Race-safe discard (CAS): only clear if the refresh token we used is STILL
|
|
215
|
+
// current. A concurrent refresh may have already rotated it — our 401 is then
|
|
216
|
+
// stale/expected, and wiping here would silently log out a healthy session
|
|
217
|
+
// (the rotation-race wipe, QA 6873 site 1/2). Compare-and-skip if it changed;
|
|
218
|
+
// a concurrent rotation is NOT a terminal condition.
|
|
219
|
+
const postRefresh = await (0, session_store_1.getSession)(sessionToken);
|
|
220
|
+
// CAS compares against the token we ACTUALLY SENT (WO-2: sentRefreshToken,
|
|
221
|
+
// the post-lock value), not the pre-lock snapshot — otherwise a token rotated
|
|
222
|
+
// between pre-lock read and our send would be misjudged.
|
|
223
|
+
const tokenUnchanged = !!sentRefreshToken && postRefresh?.idpRefreshToken === sentRefreshToken;
|
|
224
|
+
if (tokenUnchanged) {
|
|
225
|
+
await (0, session_store_1.updateSession)(sessionToken, {
|
|
226
|
+
idpRefreshToken: '',
|
|
227
|
+
idpRefreshTokenExpires: undefined,
|
|
228
|
+
refreshTokenClearedAt: Date.now(),
|
|
229
|
+
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${code}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// Concurrent refresh already rotated the token — our 401 is stale. Return the
|
|
234
|
+
// freshly-rotated token directly so the loser succeeds without a retry round-trip
|
|
235
|
+
// (race-fix follow-up #1).
|
|
236
|
+
discardToken = false;
|
|
237
|
+
if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
|
|
238
|
+
return {
|
|
239
|
+
ok: true,
|
|
240
|
+
accessToken: postRefresh.idpAccessToken,
|
|
241
|
+
accessTokenExpires: postRefresh.idpAccessTokenExpires,
|
|
242
|
+
refreshed: true,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
212
246
|
}
|
|
213
247
|
return {
|
|
214
248
|
ok: false,
|
package/dist/lib/idp-fetch.js
CHANGED
|
@@ -67,6 +67,14 @@ async function ensureFreshAccessToken(req) {
|
|
|
67
67
|
return {};
|
|
68
68
|
}
|
|
69
69
|
let session = await (0, session_store_1.getSession)(sessionToken);
|
|
70
|
+
// Fall back to Better Auth Redis key when app session is missing/stale
|
|
71
|
+
if (!session || !session.idpAccessToken) {
|
|
72
|
+
const { getBetterAuthSession } = await Promise.resolve().then(() => __importStar(require('./session-store')));
|
|
73
|
+
const baSession = await getBetterAuthSession(sessionToken);
|
|
74
|
+
if (baSession?.idpAccessToken) {
|
|
75
|
+
session = baSession;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
70
78
|
// Fall back to Better Auth: decode session from cookies to get IDP tokens
|
|
71
79
|
if (!session?.idpAccessToken) {
|
|
72
80
|
try {
|
|
@@ -81,28 +89,34 @@ async function ensureFreshAccessToken(req) {
|
|
|
81
89
|
console.log('[IDP_FETCH] Redis session lookup:', {
|
|
82
90
|
hasSession: !!session,
|
|
83
91
|
hasAccessToken: !!session?.idpAccessToken,
|
|
92
|
+
hasRefreshToken: !!session?.idpRefreshToken,
|
|
84
93
|
bearerKeyId: session?.bearerKeyId || 'NOT_SET',
|
|
85
94
|
idpClientId: session?.idpClientId || 'NOT_SET',
|
|
86
95
|
});
|
|
87
96
|
if (!session?.idpAccessToken)
|
|
88
97
|
return { sessionToken };
|
|
98
|
+
const hasRefreshToken = !!session.idpRefreshToken;
|
|
89
99
|
const now = Date.now();
|
|
90
100
|
const timeLeft = (session.idpAccessTokenExpires ?? now) - now;
|
|
91
101
|
// Treat tokens as effectively expired if within 5 minutes of expiry
|
|
92
102
|
if (timeLeft > 5 * 60 * 1000) {
|
|
93
|
-
return { sessionToken, accessToken: session.idpAccessToken };
|
|
103
|
+
return { sessionToken, accessToken: session.idpAccessToken, hasRefreshToken };
|
|
94
104
|
}
|
|
95
|
-
// attempt refresh
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
// Only attempt refresh when we actually have a refresh token to use.
|
|
106
|
+
// Provisional tokens (issued before 2FA completion) have no refresh token.
|
|
107
|
+
if (hasRefreshToken) {
|
|
108
|
+
try {
|
|
109
|
+
await (0, internal_api_1.internalRefresh)(req.headers.get('cookie') || '', sessionToken);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
session = await (0, session_store_1.getSession)(sessionToken);
|
|
113
|
+
return { sessionToken, accessToken: session?.idpAccessToken, hasRefreshToken: !!session?.idpRefreshToken };
|
|
98
114
|
}
|
|
99
|
-
|
|
100
|
-
session = await (0, session_store_1.getSession)(sessionToken);
|
|
101
|
-
return { sessionToken, accessToken: session?.idpAccessToken };
|
|
115
|
+
return { sessionToken, accessToken: session.idpAccessToken, hasRefreshToken };
|
|
102
116
|
}
|
|
103
117
|
async function idpFetchJSON(req, targetUrl, init = {}) {
|
|
104
118
|
let attemptedRefresh = false;
|
|
105
|
-
let { accessToken, sessionToken } = await ensureFreshAccessToken(req);
|
|
119
|
+
let { accessToken, sessionToken, hasRefreshToken } = await ensureFreshAccessToken(req);
|
|
106
120
|
const makeCall = async (bearer) => {
|
|
107
121
|
const res = await fetch(targetUrl, {
|
|
108
122
|
...init,
|
|
@@ -119,7 +133,9 @@ async function idpFetchJSON(req, targetUrl, init = {}) {
|
|
|
119
133
|
};
|
|
120
134
|
// First attempt
|
|
121
135
|
let { res, json } = await makeCall(accessToken);
|
|
122
|
-
|
|
136
|
+
// Only attempt refresh on 401/403 if we actually have a refresh token.
|
|
137
|
+
// Provisional tokens (pre-2FA) have no refresh token — refreshing is futile.
|
|
138
|
+
if ((res.status === 401 || res.status === 403) && sessionToken && hasRefreshToken && !attemptedRefresh) {
|
|
123
139
|
attemptedRefresh = true;
|
|
124
140
|
try {
|
|
125
141
|
// Use centralized internal API helper for server-to-server calls
|