@payez/next-mvp 4.0.46 → 4.0.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/api/auth-handler.d.ts +0 -2
  2. package/dist/api/auth-handler.js +1 -1
  3. package/dist/api-handlers/admin/stats.js +24 -14
  4. package/dist/api-handlers/auth/refresh.d.ts +4 -6
  5. package/dist/api-handlers/auth/refresh.js +5 -7
  6. package/dist/api-handlers/auth/signout.d.ts +6 -15
  7. package/dist/api-handlers/auth/signout.js +9 -16
  8. package/dist/api-handlers/auth/update-session.d.ts +6 -15
  9. package/dist/api-handlers/auth/update-session.js +7 -15
  10. package/dist/api-handlers/auth/verify-code.d.ts +6 -15
  11. package/dist/api-handlers/auth/verify-code.js +7 -15
  12. package/dist/api-handlers/session/viability.js +2 -2
  13. package/dist/auth/better-auth.d.ts +3 -19
  14. package/dist/auth/better-auth.js +7 -13
  15. package/dist/client/better-auth-client.d.ts +7 -8
  16. package/dist/client/better-auth-client.js +3 -4
  17. package/dist/lib/auth-secret.d.ts +17 -0
  18. package/dist/lib/{nextauth-secret.js → auth-secret.js} +31 -15
  19. package/dist/lib/demo-mode.js +3 -1
  20. package/dist/lib/idp-client-config.d.ts +6 -2
  21. package/dist/lib/idp-client-config.js +35 -21
  22. package/dist/lib/secret-validation.d.ts +1 -1
  23. package/dist/lib/secret-validation.js +2 -2
  24. package/dist/lib/startup-init.d.ts +3 -3
  25. package/dist/lib/startup-init.js +23 -18
  26. package/dist/lib/test-aware-get-token.js +2 -51
  27. package/dist/routes/account/masked-info.d.ts +1 -1
  28. package/dist/routes/account/masked-info.js +1 -1
  29. package/dist/routes/account/send-code.d.ts +1 -1
  30. package/dist/routes/account/send-code.js +1 -1
  31. package/dist/routes/account/verify-email.d.ts +1 -1
  32. package/dist/routes/account/verify-email.js +1 -1
  33. package/dist/routes/account/verify-sms.d.ts +1 -1
  34. package/dist/routes/account/verify-sms.js +1 -1
  35. package/dist/routes/auth/refresh.js +3 -8
  36. package/dist/server/auth.d.ts +28 -7
  37. package/dist/server/auth.js +106 -55
  38. package/dist/server/decode-session.js +2 -2
  39. package/dist/vibe/hooks/index.d.ts +1 -1
  40. package/package.json +888 -893
  41. package/src/api/auth-handler.ts +0 -4
  42. package/src/api-handlers/admin/stats.ts +249 -238
  43. package/src/api-handlers/auth/refresh.ts +5 -8
  44. package/src/api-handlers/auth/signout.ts +9 -21
  45. package/src/api-handlers/auth/update-session.ts +7 -20
  46. package/src/api-handlers/auth/verify-code.ts +7 -20
  47. package/src/api-handlers/session/viability.ts +2 -2
  48. package/src/auth/better-auth.ts +7 -32
  49. package/src/client/better-auth-client.ts +3 -4
  50. package/src/lib/{nextauth-secret.ts → auth-secret.ts} +32 -16
  51. package/src/lib/demo-mode.ts +5 -1
  52. package/src/lib/idp-client-config.ts +42 -22
  53. package/src/lib/secret-validation.ts +1 -1
  54. package/src/lib/startup-init.ts +23 -18
  55. package/src/lib/test-aware-get-token.ts +2 -51
  56. package/src/routes/account/masked-info.ts +1 -1
  57. package/src/routes/account/send-code.ts +1 -1
  58. package/src/routes/account/verify-email.ts +1 -1
  59. package/src/routes/account/verify-sms.ts +1 -1
  60. package/src/routes/auth/refresh.ts +3 -8
  61. package/src/server/auth.ts +129 -22
  62. package/src/server/decode-session.ts +2 -2
  63. package/dist/lib/nextauth-secret.d.ts +0 -10
@@ -50,9 +50,6 @@ export interface AuthHandlerOptions {
50
50
  /** Maximum number of retry attempts on 401 (default: 1) */
51
51
  maxRetries?: number;
52
52
 
53
- /** NextAuth secret for JWT decoding */
54
- nextAuthSecret?: string;
55
-
56
53
  /** IDP base URL for refresh requests */
57
54
  idpBaseUrl?: string;
58
55
 
@@ -99,7 +96,6 @@ export function createAuthHandler(options: AuthHandlerOptions = {}) {
99
96
  refreshBuffer = 60, // 60 seconds - matches website-membership proven threshold
100
97
  retryOn401 = true,
101
98
  maxRetries = 1,
102
- nextAuthSecret = process.env.NEXTAUTH_SECRET,
103
99
  idpBaseUrl = process.env.IDP_URL,
104
100
  clientId = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_IDP_CLIENT_ID
105
101
  } = options;
@@ -1,238 +1,249 @@
1
- /**
2
- * Admin Stats API Handler
3
- *
4
- * Aggregates dashboard statistics from users, Redis sessions, and audit logs.
5
- * Uses service account HMAC auth for Vibe API requests.
6
- *
7
- * @version 1.0
8
- * @requires Admin role (vibe_app_admin or payez_admin)
9
- */
10
-
11
- import { NextRequest, NextResponse } from 'next/server';
12
- import { getSession } from '../../server/auth';
13
- import { getStartupIDPConfig } from '../../lib/startup-init';
14
- import { getRedis } from '../../lib/redis';
15
- import { ADMIN_ROLES } from '../../lib/roles';
16
-
17
- interface VibeRequestOptions {
18
- method: 'GET' | 'POST' | 'PUT' | 'DELETE';
19
- body?: unknown;
20
- }
21
-
22
- async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean; error?: NextResponse }> {
23
- const session = await getSession(request) as any;
24
-
25
- if (!session?.user) {
26
- return {
27
- isAdmin: false,
28
- error: NextResponse.json({ success: false, error: 'Please sign in' }, { status: 401 }),
29
- };
30
- }
31
-
32
- const userRoles = (session.user?.roles as string[]) || [];
33
- const hasAdminRole = ADMIN_ROLES.some(role => userRoles.includes(role));
34
-
35
- if (!hasAdminRole) {
36
- return {
37
- isAdmin: false,
38
- error: NextResponse.json({ success: false, error: 'Admin access required' }, { status: 403 }),
39
- };
40
- }
41
-
42
- return { isAdmin: true };
43
- }
44
-
45
- async function vibeServiceRequest<T = unknown>(
46
- endpoint: string,
47
- options: VibeRequestOptions
48
- ): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
49
- const idpUrl = process.env.NEXT_PUBLIC_IDP_URL || process.env.IDP_URL;
50
- const clientId = process.env.VIBE_CLIENT_ID;
51
- const signingKey = process.env.VIBE_HMAC_KEY;
52
-
53
- if (!idpUrl || !clientId || !signingKey) {
54
- return { ok: false, status: 500, data: null, error: 'Vibe not configured' };
55
- }
56
-
57
- const timestamp = Math.floor(Date.now() / 1000);
58
- const stringToSign = `${timestamp}|${options.method}|${endpoint}`;
59
-
60
- const crypto = await import('crypto');
61
- const signature = crypto
62
- .createHmac('sha256', Buffer.from(signingKey, 'base64'))
63
- .update(stringToSign)
64
- .digest('base64');
65
-
66
- const proxyUrl = `${idpUrl}/api/vibe/proxy`;
67
-
68
- const idpConfig = getStartupIDPConfig();
69
- const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
70
-
71
- try {
72
- const res = await fetch(proxyUrl, {
73
- method: 'POST',
74
- headers: {
75
- 'Content-Type': 'application/json',
76
- 'X-Vibe-Client-Id': clientId,
77
- 'X-Vibe-Timestamp': String(timestamp),
78
- 'X-Vibe-Signature': signature,
79
- ...(idpClientId && { 'X-Client-Id': idpClientId }),
80
- },
81
- body: JSON.stringify({
82
- endpoint,
83
- method: options.method,
84
- data: options.body ?? null,
85
- }),
86
- cache: 'no-store',
87
- });
88
-
89
- if (res.status === 204) return { ok: true, status: 204, data: null };
90
- if (!res.ok) {
91
- const errorText = await res.text();
92
- return { ok: false, status: res.status, data: null, error: errorText };
93
- }
94
-
95
- const body = await res.json();
96
- return { ok: true, status: res.status, data: body };
97
- } catch (error) {
98
- return { ok: false, status: 0, data: null, error: String(error) };
99
- }
100
- }
101
-
102
- export interface AdminStatsHandlerConfig {
103
- appSlug?: string;
104
- }
105
-
106
- /**
107
- * GET /api/admin/stats - Dashboard statistics
108
- * Aggregates users + tier breakdown, active Redis sessions, and recent audit activity.
109
- */
110
- export function createStatsHandler(config: AdminStatsHandlerConfig) {
111
- const getSessionPrefix = () => {
112
- const appSlug = config.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'app';
113
- return `${appSlug}:sess:`;
114
- };
115
-
116
- return {
117
- async GET(_request: NextRequest) {
118
- const adminCheck = await checkAdminRole(_request);
119
- if (adminCheck.error) return adminCheck.error;
120
-
121
- try {
122
- // Fetch from 3 sources in parallel
123
- const [usersResult, sessionCount, auditResult] = await Promise.allSettled([
124
- // 1. Users + tier breakdown via HMAC proxy (Vibe collection query)
125
- vibeServiceRequest<any>('/v1/collections/vibe_app/tables/users/query', {
126
- method: 'POST',
127
- body: { page: 1, pageSize: 500, orderBy: 'created_at', orderDirection: 'desc' },
128
- }),
129
-
130
- // 2. Active sessions from Redis
131
- (async () => {
132
- const redis = getRedis();
133
- const sessionPrefix = getSessionPrefix();
134
- const sessionKeys: string[] = [];
135
- let cursor = '0';
136
- do {
137
- const [newCursor, keys] = await redis.scan(cursor, 'MATCH', `${sessionPrefix}*`, 'COUNT', 100);
138
- cursor = newCursor;
139
- sessionKeys.push(...keys.filter((k: string) => !k.includes(':ver:')));
140
- } while (cursor !== '0');
141
- return sessionKeys.length;
142
- })(),
143
-
144
- // 3. Recent audit activity via HMAC proxy
145
- vibeServiceRequest<any>('/v1/audit?pageSize=10&sortDir=desc', { method: 'GET' }),
146
- ]);
147
-
148
- // Parse users deduplicate by user_id
149
- let totalUsers = 0;
150
- let tierBreakdown: Record<string, number> = {};
151
- if (usersResult.status === 'fulfilled' && usersResult.value.ok && usersResult.value.data) {
152
- const data = usersResult.value.data;
153
- const rawUsers = data.data || data.documents || data.users || [];
154
-
155
- // Deduplicate by user_id (keeps latest document_id)
156
- const userMap = new Map();
157
- for (const u of rawUsers) {
158
- const uid = u.user_id || u.id || u.document_id;
159
- const existing = userMap.get(uid);
160
- if (!existing || (u.document_id || '') > (existing.document_id || '')) {
161
- userMap.set(uid, u);
162
- }
163
- }
164
- const uniqueUsers = Array.from(userMap.values());
165
- totalUsers = uniqueUsers.length;
166
-
167
- // Build tier breakdown from deduplicated users (unless API provides one)
168
- tierBreakdown = data.tierBreakdown || data.tiers || {};
169
- if (Object.keys(tierBreakdown).length === 0) {
170
- for (const user of uniqueUsers) {
171
- const tier = user.tier || 'free';
172
- tierBreakdown[tier] = (tierBreakdown[tier] || 0) + 1;
173
- }
174
- }
175
- }
176
-
177
- // Parse active sessions count
178
- let activeSessions = 0;
179
- if (sessionCount.status === 'fulfilled') {
180
- activeSessions = sessionCount.value;
181
- }
182
-
183
- // Parse audit events for recent activity
184
- let recentActivity: any[] = [];
185
- if (auditResult.status === 'fulfilled' && auditResult.value.ok && auditResult.value.data) {
186
- const data = auditResult.value.data;
187
-
188
- // Handle multiple possible response shapes
189
- let events: any[] = [];
190
- if (Array.isArray(data)) {
191
- events = data;
192
- } else if (Array.isArray(data.data)) {
193
- events = data.data;
194
- } else if (Array.isArray(data.entries)) {
195
- events = data.entries;
196
- } else if (Array.isArray(data.items)) {
197
- events = data.items;
198
- } else if (Array.isArray(data.documents)) {
199
- events = data.documents;
200
- } else if (data.success && Array.isArray(data.results)) {
201
- events = data.results;
202
- }
203
-
204
- recentActivity = events.slice(0, 5).map((e: any) => ({
205
- id: e.audit_log_id || e.id || e.document_id,
206
- type: e.category || e.type || 'admin',
207
- action: e.action || e.event || e.message || 'Unknown action',
208
- actor: e.admin_email || e.actor || e.user || e.actor_email || 'System',
209
- target: e.target_type ? `${e.target_type}:${e.target_id}` : (e.target || e.target_user),
210
- details: e.description || e.details,
211
- timestamp: e.created_at || e.timestamp || e.date,
212
- success: e.is_success ?? e.success ?? true,
213
- }));
214
- }
215
-
216
- // Calculate tier percentages
217
- const tiers = Object.entries(tierBreakdown).map(([name, count]) => ({
218
- name,
219
- count: count as number,
220
- pct: totalUsers > 0 ? Math.round(((count as number) / totalUsers) * 100) : 0,
221
- }));
222
-
223
- return NextResponse.json({
224
- totalUsers,
225
- activeSessions,
226
- tiers,
227
- recentActivity,
228
- });
229
- } catch (error: any) {
230
- console.error('[admin/stats] Error:', error);
231
- return NextResponse.json(
232
- { error: error.message || 'Internal error' },
233
- { status: 500 }
234
- );
235
- }
236
- },
237
- };
238
- }
1
+ /**
2
+ * Admin Stats API Handler
3
+ *
4
+ * Aggregates dashboard statistics from users, Redis sessions, and audit logs.
5
+ * Uses service account HMAC auth for Vibe API requests.
6
+ *
7
+ * @version 1.0
8
+ * @requires Admin role (vibe_app_admin or payez_admin)
9
+ */
10
+
11
+ import { NextRequest, NextResponse } from 'next/server';
12
+ import { getSession } from '../../server/auth';
13
+ import { getStartupIDPConfig } from '../../lib/startup-init';
14
+ import { getRedis } from '../../lib/redis';
15
+ import { ADMIN_ROLES } from '../../lib/roles';
16
+
17
+ interface VibeRequestOptions {
18
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
19
+ body?: unknown;
20
+ }
21
+
22
+ async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean; error?: NextResponse }> {
23
+ const session = await getSession(request) as any;
24
+
25
+ if (!session?.user) {
26
+ return {
27
+ isAdmin: false,
28
+ error: NextResponse.json({ success: false, error: 'Please sign in' }, { status: 401 }),
29
+ };
30
+ }
31
+
32
+ const userRoles = (session.user?.roles as string[]) || [];
33
+ const hasAdminRole = ADMIN_ROLES.some(role => userRoles.includes(role));
34
+
35
+ if (!hasAdminRole) {
36
+ return {
37
+ isAdmin: false,
38
+ error: NextResponse.json({ success: false, error: 'Admin access required' }, { status: 403 }),
39
+ };
40
+ }
41
+
42
+ return { isAdmin: true };
43
+ }
44
+
45
+ async function vibeServiceRequest<T = unknown>(
46
+ endpoint: string,
47
+ options: VibeRequestOptions
48
+ ): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
49
+ const idpUrl = process.env.NEXT_PUBLIC_IDP_URL || process.env.IDP_URL;
50
+ const clientId = process.env.VIBE_CLIENT_ID;
51
+ const signingKey = process.env.VIBE_HMAC_KEY;
52
+
53
+ if (!idpUrl || !clientId || !signingKey) {
54
+ return { ok: false, status: 500, data: null, error: 'Vibe not configured' };
55
+ }
56
+
57
+ const timestamp = Math.floor(Date.now() / 1000);
58
+ const stringToSign = `${timestamp}|${options.method}|${endpoint}`;
59
+
60
+ const crypto = await import('crypto');
61
+ const signature = crypto
62
+ .createHmac('sha256', Buffer.from(signingKey, 'base64'))
63
+ .update(stringToSign)
64
+ .digest('base64');
65
+
66
+ const proxyUrl = `${idpUrl}/api/vibe/proxy`;
67
+
68
+ const idpConfig = getStartupIDPConfig();
69
+ const idpClientId = idpConfig?.clientSlug || idpConfig?.clientId;
70
+
71
+ try {
72
+ const res = await fetch(proxyUrl, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ 'X-Vibe-Client-Id': clientId,
77
+ 'X-Vibe-Timestamp': String(timestamp),
78
+ 'X-Vibe-Signature': signature,
79
+ ...(idpClientId && { 'X-Client-Id': idpClientId }),
80
+ },
81
+ body: JSON.stringify({
82
+ endpoint,
83
+ method: options.method,
84
+ data: options.body ?? null,
85
+ }),
86
+ cache: 'no-store',
87
+ });
88
+
89
+ if (res.status === 204) return { ok: true, status: 204, data: null };
90
+ if (!res.ok) {
91
+ const errorText = await res.text();
92
+ return { ok: false, status: res.status, data: null, error: errorText };
93
+ }
94
+
95
+ const body = await res.json();
96
+ return { ok: true, status: res.status, data: body };
97
+ } catch (error) {
98
+ return { ok: false, status: 0, data: null, error: String(error) };
99
+ }
100
+ }
101
+
102
+ export interface AdminStatsHandlerConfig {
103
+ appSlug?: string;
104
+ }
105
+
106
+ /**
107
+ * GET /api/admin/stats - Dashboard statistics
108
+ * Aggregates users + tier breakdown, active Redis sessions, and recent audit activity.
109
+ */
110
+ export function createStatsHandler(config: AdminStatsHandlerConfig) {
111
+ const getSessionPrefix = () => {
112
+ const appSlug = config.appSlug || process.env.APP_SLUG || process.env.CLIENT_ID || 'app';
113
+ return `${appSlug}:sess:`;
114
+ };
115
+
116
+ return {
117
+ async GET(_request: NextRequest) {
118
+ const adminCheck = await checkAdminRole(_request);
119
+ if (adminCheck.error) return adminCheck.error;
120
+
121
+ try {
122
+ // Fetch from 4 sources in parallel
123
+ const [usersResult, tierDistributionResult, sessionCount, auditResult] = await Promise.allSettled([
124
+ // 1. Users count via HMAC proxy (Vibe collection query)
125
+ vibeServiceRequest<any>('/v1/collections/vibe_app/tables/users/query', {
126
+ method: 'POST',
127
+ body: { page: 1, pageSize: 500, orderBy: 'created_at', orderDirection: 'desc' },
128
+ }),
129
+
130
+ // 2. Tier distribution from analytics endpoint (uses purchases table)
131
+ vibeServiceRequest<any>('/v1/analytics/tier-distribution?includeTrend=false', { method: 'GET' }),
132
+
133
+ // 3. Active sessions from Redis
134
+ (async () => {
135
+ const redis = getRedis();
136
+ const sessionPrefix = getSessionPrefix();
137
+ const sessionKeys: string[] = [];
138
+ let cursor = '0';
139
+ do {
140
+ const [newCursor, keys] = await redis.scan(cursor, 'MATCH', `${sessionPrefix}*`, 'COUNT', 100);
141
+ cursor = newCursor;
142
+ sessionKeys.push(...keys.filter((k: string) => !k.includes(':ver:')));
143
+ } while (cursor !== '0');
144
+ return sessionKeys.length;
145
+ })(),
146
+
147
+ // 4. Recent audit activity via HMAC proxy
148
+ vibeServiceRequest<any>('/v1/audit?pageSize=10&sortDir=desc', { method: 'GET' }),
149
+ ]);
150
+
151
+ // Parse users deduplicate by user_id
152
+ let totalUsers = 0;
153
+ if (usersResult.status === 'fulfilled' && usersResult.value.ok && usersResult.value.data) {
154
+ const data = usersResult.value.data;
155
+ const rawUsers = data.data || data.documents || data.users || [];
156
+
157
+ // Deduplicate by user_id (keeps latest document_id)
158
+ const userMap = new Map();
159
+ for (const u of rawUsers) {
160
+ const uid = u.user_id || u.id || u.document_id;
161
+ const existing = userMap.get(uid);
162
+ if (!existing || (u.document_id || '') > (existing.document_id || '')) {
163
+ userMap.set(uid, u);
164
+ }
165
+ }
166
+ totalUsers = userMap.size;
167
+ }
168
+
169
+ // Parse tier distribution from analytics endpoint (uses purchases table)
170
+ let tierBreakdown: Record<string, number> = {};
171
+ if (tierDistributionResult.status === 'fulfilled' && tierDistributionResult.value.ok && tierDistributionResult.value.data) {
172
+ const data = tierDistributionResult.value.data;
173
+ // Handle response shape: { distribution: [{ tierKey, userCount }, ...] }
174
+ const distribution = data.distribution || data.data || data.tiers || [];
175
+ if (Array.isArray(distribution)) {
176
+ for (const item of distribution) {
177
+ const tierKey = item.tierKey || item.tier || item.name || 'free';
178
+ const count = item.userCount || item.count || item.users || 0;
179
+ tierBreakdown[tierKey] = (tierBreakdown[tierKey] || 0) + count;
180
+ }
181
+ }
182
+ }
183
+ // Fallback: if no tier data from analytics, show all as free
184
+ if (Object.keys(tierBreakdown).length === 0) {
185
+ tierBreakdown = { free: totalUsers };
186
+ }
187
+
188
+ // Parse active sessions count
189
+ let activeSessions = 0;
190
+ if (sessionCount.status === 'fulfilled') {
191
+ activeSessions = sessionCount.value;
192
+ }
193
+
194
+ // Parse audit events for recent activity
195
+ let recentActivity: any[] = [];
196
+ if (auditResult.status === 'fulfilled' && auditResult.value.ok && auditResult.value.data) {
197
+ const data = auditResult.value.data;
198
+
199
+ // Handle multiple possible response shapes
200
+ let events: any[] = [];
201
+ if (Array.isArray(data)) {
202
+ events = data;
203
+ } else if (Array.isArray(data.data)) {
204
+ events = data.data;
205
+ } else if (Array.isArray(data.entries)) {
206
+ events = data.entries;
207
+ } else if (Array.isArray(data.items)) {
208
+ events = data.items;
209
+ } else if (Array.isArray(data.documents)) {
210
+ events = data.documents;
211
+ } else if (data.success && Array.isArray(data.results)) {
212
+ events = data.results;
213
+ }
214
+
215
+ recentActivity = events.slice(0, 5).map((e: any) => ({
216
+ id: e.audit_log_id || e.id || e.document_id,
217
+ type: e.category || e.type || 'admin',
218
+ action: e.action || e.event || e.message || 'Unknown action',
219
+ actor: e.admin_email || e.actor || e.user || e.actor_email || 'System',
220
+ target: e.target_type ? `${e.target_type}:${e.target_id}` : (e.target || e.target_user),
221
+ details: e.description || e.details,
222
+ timestamp: e.created_at || e.timestamp || e.date,
223
+ success: e.is_success ?? e.success ?? true,
224
+ }));
225
+ }
226
+
227
+ // Calculate tier percentages
228
+ const tiers = Object.entries(tierBreakdown).map(([name, count]) => ({
229
+ name,
230
+ count: count as number,
231
+ pct: totalUsers > 0 ? Math.round(((count as number) / totalUsers) * 100) : 0,
232
+ }));
233
+
234
+ return NextResponse.json({
235
+ totalUsers,
236
+ activeSessions,
237
+ tiers,
238
+ recentActivity,
239
+ });
240
+ } catch (error: any) {
241
+ console.error('[admin/stats] Error:', error);
242
+ return NextResponse.json(
243
+ { error: error.message || 'Internal error' },
244
+ { status: 500 }
245
+ );
246
+ }
247
+ },
248
+ };
249
+ }
@@ -4,7 +4,7 @@
4
4
  * ASK BEFORE EDITING - TESTED AND WORKING SYSTEM
5
5
  *
6
6
  * This handler manages the server-side refresh token cycle with:
7
- * - NextAuth JWT token extraction
7
+ * - Better Auth session extraction
8
8
  * - Session token fallback for internal calls
9
9
  * - PayEz IDP refresh token exchange
10
10
  * - Session state updates with new tokens
@@ -23,14 +23,13 @@ import { extractKidFromToken } from '../../auth/utils/token-utils';
23
23
  interface RefreshConfig {
24
24
  idpBaseUrl: string;
25
25
  clientId: string;
26
- nextAuthSecret: string;
27
26
  refreshEndpoint?: string;
28
27
  }
29
28
 
30
29
  /**
31
30
  * Creates a refresh token handler for Next.js API routes
32
31
  *
33
- * @param config Configuration for IDP connection and NextAuth
32
+ * @param config IDP connection settings (Better Auth handles session crypto)
34
33
  * @returns Next.js POST handler function
35
34
  *
36
35
  * @example
@@ -41,13 +40,12 @@ interface RefreshConfig {
41
40
  * export const POST = createRefreshHandler({
42
41
  * idpBaseUrl: process.env.IDP_URL!,
43
42
  * clientId: process.env.CLIENT_ID!,
44
- * nextAuthSecret: process.env.NEXTAUTH_SECRET!,
45
43
  * refreshEndpoint: '/api/ExternalAuth/refresh'
46
44
  * });
47
45
  * ```
48
46
  */
49
47
  export function createRefreshHandler(config: RefreshConfig) {
50
- const { idpBaseUrl, clientId, nextAuthSecret, refreshEndpoint = '/api/ExternalAuth/refresh' } = config;
48
+ const { idpBaseUrl, clientId, refreshEndpoint = '/api/ExternalAuth/refresh' } = config;
51
49
 
52
50
  return async function POST(req: NextRequest) {
53
51
  try {
@@ -674,12 +672,11 @@ export function createRefreshHandler(config: RefreshConfig) {
674
672
  }
675
673
 
676
674
  /**
677
- * Default export for backward compatibility
678
- * Requires environment variables: IDP_URL, CLIENT_ID, NEXTAUTH_SECRET
675
+ * Default POST export — drop-in for `app/api/auth/refresh/route.ts`.
676
+ * Requires environment variables: IDP_URL, CLIENT_ID
679
677
  */
680
678
  export const POST = createRefreshHandler({
681
679
  idpBaseUrl: process.env.IDP_URL!,
682
680
  clientId: process.env.CLIENT_ID || 'payez_default_client',
683
- nextAuthSecret: process.env.NEXTAUTH_SECRET || '',
684
681
  refreshEndpoint: '/api/ExternalAuth/refresh'
685
682
  });
@@ -57,29 +57,19 @@ interface SignoutResponse {
57
57
  chunkCookiesDeleted?: number;
58
58
  }
59
59
 
60
- interface SignoutConfig {
61
- nextAuthSecret: string;
62
- }
63
-
64
60
  /**
65
- * Creates a signout handler for Next.js API routes
61
+ * Creates a signout handler for Next.js API routes.
66
62
  *
67
- * @param config Configuration for NextAuth
68
- * @returns Next.js POST handler function
63
+ * Better Auth resolves its session from cookies, so this handler takes no
64
+ * configuration. Use the default `POST` export below for typical usage.
69
65
  *
70
66
  * @example
71
67
  * ```typescript
72
68
  * // In your app's /app/api/auth/signout/route.ts
73
- * import { createSignoutHandler } from '@payez/next-mvp/api-handlers/auth/signout';
74
- *
75
- * export const POST = createSignoutHandler({
76
- * nextAuthSecret: process.env.NEXTAUTH_SECRET!
77
- * });
69
+ * export { POST } from '@payez/next-mvp/api-handlers/auth/signout';
78
70
  * ```
79
71
  */
80
- export function createSignoutHandler(config: SignoutConfig) {
81
- const { nextAuthSecret } = config;
82
-
72
+ export function createSignoutHandler() {
83
73
  return async function POST(req: NextRequest) {
84
74
  const cookieStore = await cookies();
85
75
 
@@ -113,7 +103,8 @@ export function createSignoutHandler(config: SignoutConfig) {
113
103
  const chunkCookies = cookieStore.getAll()
114
104
  .filter(cookie => cookie.name.startsWith(`${sessionCookieName}.`));
115
105
 
116
- // Decode NextAuth JWT to extract the Redis session UUID before deletion
106
+ // Decode the Better Auth session JWT to extract the Redis session UUID
107
+ // before deletion.
117
108
  let redisSessionToken: string | null = null;
118
109
 
119
110
  // First attempt: Better Auth getSession
@@ -203,9 +194,6 @@ export function createSignoutHandler(config: SignoutConfig) {
203
194
  }
204
195
 
205
196
  /**
206
- * Default export for backward compatibility
207
- * Requires environment variable: NEXTAUTH_SECRET
197
+ * Default POST export — drop-in for `app/api/auth/signout/route.ts`.
208
198
  */
209
- export const POST = createSignoutHandler({
210
- nextAuthSecret: process.env.NEXTAUTH_SECRET || ''
211
- });
199
+ export const POST = createSignoutHandler();