@payez/next-mvp 4.1.8 → 4.1.9
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/auth/better-auth.js +15 -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/idp-fetch.js +9 -25
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +1 -1
- 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 +682 -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 +15 -3
- 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/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +10 -26
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- 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/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
|
@@ -1,251 +1,251 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Admin Redis Sessions API Handler
|
|
3
|
-
*
|
|
4
|
-
* Provides admin-level access to active sessions stored in Redis.
|
|
5
|
-
* Reads directly from Redis {APP_SLUG}:sess:* pattern.
|
|
6
|
-
*
|
|
7
|
-
* Note: This is different from sessions.ts which queries Vibe login_sessions table.
|
|
8
|
-
* This handler shows real-time active sessions in Redis.
|
|
9
|
-
*
|
|
10
|
-
* @version 1.0
|
|
11
|
-
* @requires Admin role (vibe_app_admin or payez_admin)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
15
|
-
import { getSession } from '../../server/auth';
|
|
16
|
-
import { getRedis } from '../../lib/redis';
|
|
17
|
-
import { ADMIN_ROLES, hasAnyRole } from '../../lib/roles';
|
|
18
|
-
|
|
19
|
-
export interface RedisSessionsHandlerConfig {
|
|
20
|
-
appSlug?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Check if the current user has admin role
|
|
25
|
-
*/
|
|
26
|
-
async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean; userId?: number; error?: NextResponse }> {
|
|
27
|
-
const session = await getSession(request) as any;
|
|
28
|
-
|
|
29
|
-
if (!session?.user) {
|
|
30
|
-
return {
|
|
31
|
-
isAdmin: false,
|
|
32
|
-
error: NextResponse.json(
|
|
33
|
-
{ success: false, error: 'Please sign in' },
|
|
34
|
-
{ status: 401 }
|
|
35
|
-
),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const userRoles = (session.user?.roles as string[]) || [];
|
|
40
|
-
const hasAdminRole = ADMIN_ROLES.some(role => userRoles.includes(role));
|
|
41
|
-
|
|
42
|
-
if (!hasAdminRole) {
|
|
43
|
-
return {
|
|
44
|
-
isAdmin: false,
|
|
45
|
-
error: NextResponse.json(
|
|
46
|
-
{ success: false, error: 'Admin access required' },
|
|
47
|
-
{ status: 403 }
|
|
48
|
-
),
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { isAdmin: true, userId: session.user?.id };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Create Redis sessions handler
|
|
57
|
-
* GET /api/admin/redis-sessions - List all active sessions from Redis
|
|
58
|
-
*/
|
|
59
|
-
export function createRedisSessionsHandler(config: RedisSessionsHandlerConfig) {
|
|
60
|
-
const getSessionPrefix = () => {
|
|
61
|
-
const appSlug = config.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'app';
|
|
62
|
-
return `${appSlug}:sess:`;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
async GET(request: NextRequest) {
|
|
67
|
-
const adminCheck = await checkAdminRole(request);
|
|
68
|
-
if (adminCheck.error) return adminCheck.error;
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const redis = getRedis();
|
|
72
|
-
const sessionPrefix = getSessionPrefix();
|
|
73
|
-
|
|
74
|
-
// Scan Redis for all session keys (exclude version keys)
|
|
75
|
-
const sessionKeys: string[] = [];
|
|
76
|
-
let cursor = '0';
|
|
77
|
-
do {
|
|
78
|
-
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', `${sessionPrefix}*`, 'COUNT', 100);
|
|
79
|
-
cursor = newCursor;
|
|
80
|
-
// Filter out version keys (ver:) - only want actual session keys
|
|
81
|
-
sessionKeys.push(...keys.filter((k: string) => !k.includes(':ver:')));
|
|
82
|
-
} while (cursor !== '0');
|
|
83
|
-
|
|
84
|
-
// Fetch all session data
|
|
85
|
-
const sessions = [];
|
|
86
|
-
for (const key of sessionKeys) {
|
|
87
|
-
const sessionJson = await redis.get(key);
|
|
88
|
-
if (!sessionJson) continue;
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
const sessionData = JSON.parse(sessionJson);
|
|
92
|
-
const sessionToken = key.replace(sessionPrefix, '');
|
|
93
|
-
|
|
94
|
-
sessions.push({
|
|
95
|
-
id: sessionToken.substring(0, 16), // Truncated for display
|
|
96
|
-
session_token: sessionToken.substring(0, 8) + '...', // Masked
|
|
97
|
-
user_id: sessionData.userId,
|
|
98
|
-
user_name: sessionData.name,
|
|
99
|
-
user_email: sessionData.email,
|
|
100
|
-
status: 'active', // Sessions in Redis are active
|
|
101
|
-
created_at: sessionData.createdAt,
|
|
102
|
-
mfa_verified: sessionData.mfaVerified || sessionData.twoFactorComplete || false,
|
|
103
|
-
access_token_expires: sessionData.idpAccessTokenExpires
|
|
104
|
-
? new Date(sessionData.idpAccessTokenExpires).toISOString()
|
|
105
|
-
: null,
|
|
106
|
-
});
|
|
107
|
-
} catch (parseError) {
|
|
108
|
-
console.error('[admin/redis-sessions] Failed to parse session:', key, parseError);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Sort by most recent first (if created_at available)
|
|
113
|
-
sessions.sort((a, b) => {
|
|
114
|
-
if (!a.created_at || !b.created_at) return 0;
|
|
115
|
-
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Calculate stats
|
|
119
|
-
const uniqueUsers = new Set(sessions.map(s => s.user_email).filter(Boolean)).size;
|
|
120
|
-
|
|
121
|
-
return NextResponse.json({
|
|
122
|
-
sessions,
|
|
123
|
-
total: sessions.length,
|
|
124
|
-
stats: {
|
|
125
|
-
total_active: sessions.length,
|
|
126
|
-
total_revoked: 0, // Revoked sessions are deleted from Redis
|
|
127
|
-
unique_users: uniqueUsers,
|
|
128
|
-
},
|
|
129
|
-
redis_prefix: sessionPrefix,
|
|
130
|
-
});
|
|
131
|
-
} catch (error: any) {
|
|
132
|
-
console.error('[admin/redis-sessions] Error:', error);
|
|
133
|
-
return NextResponse.json(
|
|
134
|
-
{ error: error.message || 'Internal error' },
|
|
135
|
-
{ status: 500 }
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
async DELETE(request: NextRequest) {
|
|
141
|
-
const adminCheck = await checkAdminRole(request);
|
|
142
|
-
if (adminCheck.error) return adminCheck.error;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const { searchParams } = new URL(request.url);
|
|
146
|
-
const sessionToken = searchParams.get('session_token');
|
|
147
|
-
|
|
148
|
-
if (!sessionToken) {
|
|
149
|
-
return NextResponse.json(
|
|
150
|
-
{ error: 'session_token parameter required' },
|
|
151
|
-
{ status: 400 }
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const redis = getRedis();
|
|
156
|
-
const sessionPrefix = getSessionPrefix();
|
|
157
|
-
const sessionKey = `${sessionPrefix}${sessionToken}`;
|
|
158
|
-
|
|
159
|
-
// Delete the session
|
|
160
|
-
const deleted = await redis.del(sessionKey);
|
|
161
|
-
|
|
162
|
-
if (deleted === 0) {
|
|
163
|
-
return NextResponse.json(
|
|
164
|
-
{ error: 'Session not found or already deleted' },
|
|
165
|
-
{ status: 404 }
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return NextResponse.json({
|
|
170
|
-
success: true,
|
|
171
|
-
message: 'Session revoked',
|
|
172
|
-
session_token: sessionToken.substring(0, 8) + '...',
|
|
173
|
-
});
|
|
174
|
-
} catch (error: any) {
|
|
175
|
-
console.error('[admin/redis-sessions] DELETE Error:', error);
|
|
176
|
-
return NextResponse.json(
|
|
177
|
-
{ error: error.message || 'Internal error' },
|
|
178
|
-
{ status: 500 }
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Create Redis session revoke handler for specific session
|
|
187
|
-
* POST /api/admin/redis-sessions/[sessionId]/revoke
|
|
188
|
-
*/
|
|
189
|
-
export function createRedisSessionRevokeHandler(config: RedisSessionsHandlerConfig) {
|
|
190
|
-
const getSessionPrefix = () => {
|
|
191
|
-
const appSlug = config.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'app';
|
|
192
|
-
return `${appSlug}:sess:`;
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
return {
|
|
196
|
-
async POST(request: NextRequest, { params }: { params: { sessionId: string } }) {
|
|
197
|
-
const adminCheck = await checkAdminRole(request);
|
|
198
|
-
if (adminCheck.error) return adminCheck.error;
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
const sessionId = params.sessionId;
|
|
202
|
-
if (!sessionId) {
|
|
203
|
-
return NextResponse.json(
|
|
204
|
-
{ error: 'Session ID required' },
|
|
205
|
-
{ status: 400 }
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const redis = getRedis();
|
|
210
|
-
const sessionPrefix = getSessionPrefix();
|
|
211
|
-
|
|
212
|
-
// Find session key that starts with the sessionId
|
|
213
|
-
let targetKey: string | null = null;
|
|
214
|
-
let cursor = '0';
|
|
215
|
-
do {
|
|
216
|
-
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', `${sessionPrefix}*`, 'COUNT', 100);
|
|
217
|
-
cursor = newCursor;
|
|
218
|
-
for (const key of keys) {
|
|
219
|
-
const token = key.replace(sessionPrefix, '');
|
|
220
|
-
if (token.startsWith(sessionId) || token.substring(0, 16) === sessionId) {
|
|
221
|
-
targetKey = key;
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (targetKey) break;
|
|
226
|
-
} while (cursor !== '0');
|
|
227
|
-
|
|
228
|
-
if (!targetKey) {
|
|
229
|
-
return NextResponse.json(
|
|
230
|
-
{ error: 'Session not found' },
|
|
231
|
-
{ status: 404 }
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Delete the session
|
|
236
|
-
await redis.del(targetKey);
|
|
237
|
-
|
|
238
|
-
return NextResponse.json({
|
|
239
|
-
success: true,
|
|
240
|
-
message: 'Session revoked',
|
|
241
|
-
});
|
|
242
|
-
} catch (error: any) {
|
|
243
|
-
console.error('[admin/redis-sessions/revoke] Error:', error);
|
|
244
|
-
return NextResponse.json(
|
|
245
|
-
{ error: error.message || 'Internal error' },
|
|
246
|
-
{ status: 500 }
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Admin Redis Sessions API Handler
|
|
3
|
+
*
|
|
4
|
+
* Provides admin-level access to active sessions stored in Redis.
|
|
5
|
+
* Reads directly from Redis {APP_SLUG}:sess:* pattern.
|
|
6
|
+
*
|
|
7
|
+
* Note: This is different from sessions.ts which queries Vibe login_sessions table.
|
|
8
|
+
* This handler shows real-time active sessions in Redis.
|
|
9
|
+
*
|
|
10
|
+
* @version 1.0
|
|
11
|
+
* @requires Admin role (vibe_app_admin or payez_admin)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
15
|
+
import { getSession } from '../../server/auth';
|
|
16
|
+
import { getRedis } from '../../lib/redis';
|
|
17
|
+
import { ADMIN_ROLES, hasAnyRole } from '../../lib/roles';
|
|
18
|
+
|
|
19
|
+
export interface RedisSessionsHandlerConfig {
|
|
20
|
+
appSlug?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if the current user has admin role
|
|
25
|
+
*/
|
|
26
|
+
async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean; userId?: number; error?: NextResponse }> {
|
|
27
|
+
const session = await getSession(request) as any;
|
|
28
|
+
|
|
29
|
+
if (!session?.user) {
|
|
30
|
+
return {
|
|
31
|
+
isAdmin: false,
|
|
32
|
+
error: NextResponse.json(
|
|
33
|
+
{ success: false, error: 'Please sign in' },
|
|
34
|
+
{ status: 401 }
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const userRoles = (session.user?.roles as string[]) || [];
|
|
40
|
+
const hasAdminRole = ADMIN_ROLES.some(role => userRoles.includes(role));
|
|
41
|
+
|
|
42
|
+
if (!hasAdminRole) {
|
|
43
|
+
return {
|
|
44
|
+
isAdmin: false,
|
|
45
|
+
error: NextResponse.json(
|
|
46
|
+
{ success: false, error: 'Admin access required' },
|
|
47
|
+
{ status: 403 }
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { isAdmin: true, userId: session.user?.id };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create Redis sessions handler
|
|
57
|
+
* GET /api/admin/redis-sessions - List all active sessions from Redis
|
|
58
|
+
*/
|
|
59
|
+
export function createRedisSessionsHandler(config: RedisSessionsHandlerConfig) {
|
|
60
|
+
const getSessionPrefix = () => {
|
|
61
|
+
const appSlug = config.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'app';
|
|
62
|
+
return `${appSlug}:sess:`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
async GET(request: NextRequest) {
|
|
67
|
+
const adminCheck = await checkAdminRole(request);
|
|
68
|
+
if (adminCheck.error) return adminCheck.error;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const redis = getRedis();
|
|
72
|
+
const sessionPrefix = getSessionPrefix();
|
|
73
|
+
|
|
74
|
+
// Scan Redis for all session keys (exclude version keys)
|
|
75
|
+
const sessionKeys: string[] = [];
|
|
76
|
+
let cursor = '0';
|
|
77
|
+
do {
|
|
78
|
+
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', `${sessionPrefix}*`, 'COUNT', 100);
|
|
79
|
+
cursor = newCursor;
|
|
80
|
+
// Filter out version keys (ver:) - only want actual session keys
|
|
81
|
+
sessionKeys.push(...keys.filter((k: string) => !k.includes(':ver:')));
|
|
82
|
+
} while (cursor !== '0');
|
|
83
|
+
|
|
84
|
+
// Fetch all session data
|
|
85
|
+
const sessions = [];
|
|
86
|
+
for (const key of sessionKeys) {
|
|
87
|
+
const sessionJson = await redis.get(key);
|
|
88
|
+
if (!sessionJson) continue;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const sessionData = JSON.parse(sessionJson);
|
|
92
|
+
const sessionToken = key.replace(sessionPrefix, '');
|
|
93
|
+
|
|
94
|
+
sessions.push({
|
|
95
|
+
id: sessionToken.substring(0, 16), // Truncated for display
|
|
96
|
+
session_token: sessionToken.substring(0, 8) + '...', // Masked
|
|
97
|
+
user_id: sessionData.userId,
|
|
98
|
+
user_name: sessionData.name,
|
|
99
|
+
user_email: sessionData.email,
|
|
100
|
+
status: 'active', // Sessions in Redis are active
|
|
101
|
+
created_at: sessionData.createdAt,
|
|
102
|
+
mfa_verified: sessionData.mfaVerified || sessionData.twoFactorComplete || false,
|
|
103
|
+
access_token_expires: sessionData.idpAccessTokenExpires
|
|
104
|
+
? new Date(sessionData.idpAccessTokenExpires).toISOString()
|
|
105
|
+
: null,
|
|
106
|
+
});
|
|
107
|
+
} catch (parseError) {
|
|
108
|
+
console.error('[admin/redis-sessions] Failed to parse session:', key, parseError);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sort by most recent first (if created_at available)
|
|
113
|
+
sessions.sort((a, b) => {
|
|
114
|
+
if (!a.created_at || !b.created_at) return 0;
|
|
115
|
+
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Calculate stats
|
|
119
|
+
const uniqueUsers = new Set(sessions.map(s => s.user_email).filter(Boolean)).size;
|
|
120
|
+
|
|
121
|
+
return NextResponse.json({
|
|
122
|
+
sessions,
|
|
123
|
+
total: sessions.length,
|
|
124
|
+
stats: {
|
|
125
|
+
total_active: sessions.length,
|
|
126
|
+
total_revoked: 0, // Revoked sessions are deleted from Redis
|
|
127
|
+
unique_users: uniqueUsers,
|
|
128
|
+
},
|
|
129
|
+
redis_prefix: sessionPrefix,
|
|
130
|
+
});
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
console.error('[admin/redis-sessions] Error:', error);
|
|
133
|
+
return NextResponse.json(
|
|
134
|
+
{ error: error.message || 'Internal error' },
|
|
135
|
+
{ status: 500 }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async DELETE(request: NextRequest) {
|
|
141
|
+
const adminCheck = await checkAdminRole(request);
|
|
142
|
+
if (adminCheck.error) return adminCheck.error;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const { searchParams } = new URL(request.url);
|
|
146
|
+
const sessionToken = searchParams.get('session_token');
|
|
147
|
+
|
|
148
|
+
if (!sessionToken) {
|
|
149
|
+
return NextResponse.json(
|
|
150
|
+
{ error: 'session_token parameter required' },
|
|
151
|
+
{ status: 400 }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const redis = getRedis();
|
|
156
|
+
const sessionPrefix = getSessionPrefix();
|
|
157
|
+
const sessionKey = `${sessionPrefix}${sessionToken}`;
|
|
158
|
+
|
|
159
|
+
// Delete the session
|
|
160
|
+
const deleted = await redis.del(sessionKey);
|
|
161
|
+
|
|
162
|
+
if (deleted === 0) {
|
|
163
|
+
return NextResponse.json(
|
|
164
|
+
{ error: 'Session not found or already deleted' },
|
|
165
|
+
{ status: 404 }
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return NextResponse.json({
|
|
170
|
+
success: true,
|
|
171
|
+
message: 'Session revoked',
|
|
172
|
+
session_token: sessionToken.substring(0, 8) + '...',
|
|
173
|
+
});
|
|
174
|
+
} catch (error: any) {
|
|
175
|
+
console.error('[admin/redis-sessions] DELETE Error:', error);
|
|
176
|
+
return NextResponse.json(
|
|
177
|
+
{ error: error.message || 'Internal error' },
|
|
178
|
+
{ status: 500 }
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create Redis session revoke handler for specific session
|
|
187
|
+
* POST /api/admin/redis-sessions/[sessionId]/revoke
|
|
188
|
+
*/
|
|
189
|
+
export function createRedisSessionRevokeHandler(config: RedisSessionsHandlerConfig) {
|
|
190
|
+
const getSessionPrefix = () => {
|
|
191
|
+
const appSlug = config.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'app';
|
|
192
|
+
return `${appSlug}:sess:`;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
async POST(request: NextRequest, { params }: { params: { sessionId: string } }) {
|
|
197
|
+
const adminCheck = await checkAdminRole(request);
|
|
198
|
+
if (adminCheck.error) return adminCheck.error;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const sessionId = params.sessionId;
|
|
202
|
+
if (!sessionId) {
|
|
203
|
+
return NextResponse.json(
|
|
204
|
+
{ error: 'Session ID required' },
|
|
205
|
+
{ status: 400 }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const redis = getRedis();
|
|
210
|
+
const sessionPrefix = getSessionPrefix();
|
|
211
|
+
|
|
212
|
+
// Find session key that starts with the sessionId
|
|
213
|
+
let targetKey: string | null = null;
|
|
214
|
+
let cursor = '0';
|
|
215
|
+
do {
|
|
216
|
+
const [newCursor, keys] = await redis.scan(cursor, 'MATCH', `${sessionPrefix}*`, 'COUNT', 100);
|
|
217
|
+
cursor = newCursor;
|
|
218
|
+
for (const key of keys) {
|
|
219
|
+
const token = key.replace(sessionPrefix, '');
|
|
220
|
+
if (token.startsWith(sessionId) || token.substring(0, 16) === sessionId) {
|
|
221
|
+
targetKey = key;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (targetKey) break;
|
|
226
|
+
} while (cursor !== '0');
|
|
227
|
+
|
|
228
|
+
if (!targetKey) {
|
|
229
|
+
return NextResponse.json(
|
|
230
|
+
{ error: 'Session not found' },
|
|
231
|
+
{ status: 404 }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Delete the session
|
|
236
|
+
await redis.del(targetKey);
|
|
237
|
+
|
|
238
|
+
return NextResponse.json({
|
|
239
|
+
success: true,
|
|
240
|
+
message: 'Session revoked',
|
|
241
|
+
});
|
|
242
|
+
} catch (error: any) {
|
|
243
|
+
console.error('[admin/redis-sessions/revoke] Error:', error);
|
|
244
|
+
return NextResponse.json(
|
|
245
|
+
{ error: error.message || 'Internal error' },
|
|
246
|
+
{ status: 500 }
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|