@payez/next-mvp 4.1.11 → 4.1.13

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 (92) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.d.ts +0 -3
  3. package/dist/auth/better-auth.js +164 -69
  4. package/dist/client/better-auth-client.d.ts +14 -2
  5. package/dist/lib/ensure-fresh-access-token.js +8 -42
  6. package/dist/lib/session-store.d.ts +0 -1
  7. package/dist/lib/session-store.js +29 -33
  8. package/dist/lib/startup-init.js +19 -19
  9. package/dist/lib/token-lifecycle.d.ts +0 -2
  10. package/dist/lib/token-lifecycle.js +3 -21
  11. package/package.json +5 -4
  12. package/scripts/check-internal-url-usage.sh +73 -73
  13. package/src/api/index.ts +17 -17
  14. package/src/api-handlers/account/masked-info.ts +45 -45
  15. package/src/api-handlers/account/profile.ts +80 -80
  16. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  17. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  18. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  19. package/src/api-handlers/account/reset-password.ts +23 -23
  20. package/src/api-handlers/account/send-code.ts +76 -76
  21. package/src/api-handlers/account/validate-password.ts +118 -118
  22. package/src/api-handlers/admin/stats.ts +249 -249
  23. package/src/api-handlers/anon/preferences.ts +123 -123
  24. package/src/api-handlers/auth/jwks.ts +20 -20
  25. package/src/api-handlers/auth/login.ts +240 -240
  26. package/src/api-handlers/auth/refresh.ts +15 -38
  27. package/src/api-handlers/auth/validate.ts +43 -43
  28. package/src/api-handlers/session/viability.ts +142 -142
  29. package/src/auth/better-auth.ts +130 -29
  30. package/src/auth/unauthenticated-routes.ts +2 -2
  31. package/src/client/better-auth-client.ts +70 -70
  32. package/src/client/fetchWithSession.ts +21 -21
  33. package/src/client/index.ts +13 -13
  34. package/src/client/useAnonSession.ts +131 -131
  35. package/src/components/admin/VibeAdminContext.tsx +87 -87
  36. package/src/config/logging-config.ts +82 -82
  37. package/src/config/unauthenticated-routes.ts +19 -19
  38. package/src/edge/internal-api-url.ts +65 -65
  39. package/src/edge/middleware.ts +42 -42
  40. package/src/hooks/useProfile.ts +75 -75
  41. package/src/lib/anon-session.ts +213 -213
  42. package/src/lib/auth-secret.ts +137 -137
  43. package/src/lib/demo-mode.ts +17 -17
  44. package/src/lib/ensure-fresh-access-token.ts +8 -42
  45. package/src/lib/idp-client-config.ts +559 -559
  46. package/src/lib/idp-fetch.ts +126 -126
  47. package/src/lib/internal-api.ts +171 -171
  48. package/src/lib/jwt-decode-client.ts +45 -45
  49. package/src/lib/rate-limit-service.ts +9 -9
  50. package/src/lib/redis.ts +26 -26
  51. package/src/lib/refresh-token-validator.ts +64 -64
  52. package/src/lib/secret-validation.ts +8 -8
  53. package/src/lib/session-store.ts +692 -695
  54. package/src/lib/session.ts +34 -34
  55. package/src/lib/startup-init.ts +251 -251
  56. package/src/lib/token-expiry.ts +40 -40
  57. package/src/lib/token-lifecycle.ts +470 -497
  58. package/src/lib/types/api-responses.ts +336 -336
  59. package/src/middleware/auth-decision.ts +43 -43
  60. package/src/models/DecodedAccessToken.ts +16 -16
  61. package/src/models/SessionModel.ts +264 -264
  62. package/src/pages/admin-roles/index.ts +9 -9
  63. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  64. package/src/routes/account/index.ts +31 -31
  65. package/src/routes/account/masked-info.ts +36 -36
  66. package/src/routes/account/send-code.ts +39 -39
  67. package/src/routes/account/verify-email.ts +40 -40
  68. package/src/routes/account/verify-sms.ts +40 -40
  69. package/src/routes/auth/index.ts +22 -22
  70. package/src/routes/auth/refresh.ts +48 -48
  71. package/src/routes/auth/session.ts +166 -166
  72. package/src/routes/index.ts +17 -17
  73. package/src/routes/session/index.ts +7 -7
  74. package/src/routes/session/refresh-viability.ts +17 -17
  75. package/src/server/auth.ts +373 -373
  76. package/src/server/decode-session.ts +202 -202
  77. package/src/services/signalrActivityService.ts +258 -258
  78. package/src/theme/ThemeProvider.tsx +39 -39
  79. package/src/theme/default.ts +33 -33
  80. package/src/theme/index.ts +31 -31
  81. package/src/theme/types.ts +69 -69
  82. package/src/theme/useTheme.ts +57 -57
  83. package/src/theme/utils.ts +40 -40
  84. package/src/types/api.ts +12 -12
  85. package/src/types/auth.d.ts +23 -23
  86. package/src/types/auth.ts +22 -22
  87. package/src/types/logging.ts +11 -11
  88. package/src/types/recovery.ts +54 -54
  89. package/src/utils/api.ts +353 -353
  90. package/src/utils/circuitBreaker.ts +40 -40
  91. package/src/utils/error-message.ts +108 -108
  92. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,70 +1,70 @@
1
- /**
2
- * Better Auth Client.
3
- *
4
- * Import from '@payez/next-mvp/client/better-auth-client'.
5
- *
6
- * Includes useSessionCompat() — returns a { data, status } shape so existing
7
- * components written against the legacy hook don't need destructure changes.
8
- */
9
-
10
- import { createAuthClient } from 'better-auth/react';
11
- import { magicLinkClient } from 'better-auth/client/plugins';
12
- import { useMemo } from 'react';
13
-
14
- export const authClient = createAuthClient({
15
- // baseURL derived from BETTER_AUTH_URL or window.location.origin
16
- plugins: [magicLinkClient()],
17
- });
18
-
19
- // Convenience exports
20
- export const { useSession, signIn, signOut } = authClient;
21
-
22
-
23
- /**
24
- * NextAuth-compatible useSession wrapper.
25
- *
26
- * Maps Better Auth's { data, error, isPending } to NextAuth's { data, status, update }.
27
- * Drop-in replacement — no destructure changes needed in consuming components.
28
- */
29
- export function useSessionCompat() {
30
- const baSession = authClient.useSession();
31
-
32
- const status = useMemo(() => {
33
- if (baSession.isPending) return 'loading' as const;
34
- if (baSession.data) return 'authenticated' as const;
35
- return 'unauthenticated' as const;
36
- }, [baSession.isPending, baSession.data]);
37
-
38
- // Map Better Auth session shape to NextAuth session shape
39
- const data = useMemo(() => {
40
- if (!baSession.data) return null;
41
- return {
42
- ...baSession.data,
43
- user: baSession.data.user,
44
- expires: '', // Better Auth handles expiry differently
45
- };
46
- }, [baSession.data]);
47
-
48
- return {
49
- data,
50
- status,
51
- update: async () => {
52
- // Better Auth doesn't have a direct "refresh session" call.
53
- // Force refetch by invalidating the query.
54
- // TODO: Wire to proper session refresh when available.
55
- return data;
56
- },
57
- };
58
- }
59
-
60
- /**
61
- * NextAuth-compatible signOut wrapper.
62
- *
63
- * Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
64
- */
65
- export async function signOutCompat(options?: { redirect?: boolean; callbackUrl?: string }) {
66
- await authClient.signOut();
67
- if (options?.redirect !== false && options?.callbackUrl) {
68
- window.location.href = options.callbackUrl;
69
- }
70
- }
1
+ /**
2
+ * Better Auth Client.
3
+ *
4
+ * Import from '@payez/next-mvp/client/better-auth-client'.
5
+ *
6
+ * Includes useSessionCompat() — returns a { data, status } shape so existing
7
+ * components written against the legacy hook don't need destructure changes.
8
+ */
9
+
10
+ import { createAuthClient } from 'better-auth/react';
11
+ import { magicLinkClient } from 'better-auth/client/plugins';
12
+ import { useMemo } from 'react';
13
+
14
+ export const authClient = createAuthClient({
15
+ // baseURL derived from BETTER_AUTH_URL or window.location.origin
16
+ plugins: [magicLinkClient()],
17
+ });
18
+
19
+ // Convenience exports
20
+ export const { useSession, signIn, signOut } = authClient;
21
+
22
+
23
+ /**
24
+ * NextAuth-compatible useSession wrapper.
25
+ *
26
+ * Maps Better Auth's { data, error, isPending } to NextAuth's { data, status, update }.
27
+ * Drop-in replacement — no destructure changes needed in consuming components.
28
+ */
29
+ export function useSessionCompat() {
30
+ const baSession = authClient.useSession();
31
+
32
+ const status = useMemo(() => {
33
+ if (baSession.isPending) return 'loading' as const;
34
+ if (baSession.data) return 'authenticated' as const;
35
+ return 'unauthenticated' as const;
36
+ }, [baSession.isPending, baSession.data]);
37
+
38
+ // Map Better Auth session shape to NextAuth session shape
39
+ const data = useMemo(() => {
40
+ if (!baSession.data) return null;
41
+ return {
42
+ ...baSession.data,
43
+ user: baSession.data.user,
44
+ expires: '', // Better Auth handles expiry differently
45
+ };
46
+ }, [baSession.data]);
47
+
48
+ return {
49
+ data,
50
+ status,
51
+ update: async () => {
52
+ // Better Auth doesn't have a direct "refresh session" call.
53
+ // Force refetch by invalidating the query.
54
+ // TODO: Wire to proper session refresh when available.
55
+ return data;
56
+ },
57
+ };
58
+ }
59
+
60
+ /**
61
+ * NextAuth-compatible signOut wrapper.
62
+ *
63
+ * Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
64
+ */
65
+ export async function signOutCompat(options?: { redirect?: boolean; callbackUrl?: string }) {
66
+ await authClient.signOut();
67
+ if (options?.redirect !== false && options?.callbackUrl) {
68
+ window.location.href = options.callbackUrl;
69
+ }
70
+ }
@@ -1,21 +1,21 @@
1
- import { useRef } from 'react';
2
-
3
- export async function fetchWithSession(input: RequestInfo | URL, init: RequestInit = {}, opts: { retry?: number } = {}): Promise<Response> {
4
- const retry = opts.retry ?? 1;
5
- const doFetch = async (): Promise<Response> => {
6
- const res = await fetch(input, { ...init, credentials: 'include' });
7
- if (res.ok) return res;
8
- if (res.status === 401 && retry > 0) {
9
- const rf = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'include' });
10
- if (rf.ok) return fetchWithSession(input, init, { retry: retry - 1 });
11
- }
12
- if ((res.status === 409 || res.status === 503) && retry > 0) {
13
- const retryAfter = res.headers.get('Retry-After');
14
- const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
15
- await new Promise(r => setTimeout(r, Math.min(waitMs, 3000)));
16
- return fetchWithSession(input, init, { retry: retry - 1 });
17
- }
18
- return res;
19
- };
20
- return doFetch();
21
- }
1
+ import { useRef } from 'react';
2
+
3
+ export async function fetchWithSession(input: RequestInfo | URL, init: RequestInit = {}, opts: { retry?: number } = {}): Promise<Response> {
4
+ const retry = opts.retry ?? 1;
5
+ const doFetch = async (): Promise<Response> => {
6
+ const res = await fetch(input, { ...init, credentials: 'include' });
7
+ if (res.ok) return res;
8
+ if (res.status === 401 && retry > 0) {
9
+ const rf = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'include' });
10
+ if (rf.ok) return fetchWithSession(input, init, { retry: retry - 1 });
11
+ }
12
+ if ((res.status === 409 || res.status === 503) && retry > 0) {
13
+ const retryAfter = res.headers.get('Retry-After');
14
+ const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
15
+ await new Promise(r => setTimeout(r, Math.min(waitMs, 3000)));
16
+ return fetchWithSession(input, init, { retry: retry - 1 });
17
+ }
18
+ return res;
19
+ };
20
+ return doFetch();
21
+ }
@@ -1,13 +1,13 @@
1
- /**
2
- * Client-Side Exports
3
- *
4
- * This module exports only client-safe code for use in browser environments.
5
- * Server-only utilities and Node.js dependencies are excluded.
6
- */
7
-
8
- // Client-side fetch utility
9
- export { fetchWithAuth } from './fetch-with-auth';
10
-
11
- // Authentication context and hooks
12
- export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFederatedAuthEnabled, useTraditionalAuthEnabled } from './AuthContext';
13
- export type { AuthConfig } from '../types/auth';
1
+ /**
2
+ * Client-Side Exports
3
+ *
4
+ * This module exports only client-safe code for use in browser environments.
5
+ * Server-only utilities and Node.js dependencies are excluded.
6
+ */
7
+
8
+ // Client-side fetch utility
9
+ export { fetchWithAuth } from './fetch-with-auth';
10
+
11
+ // Authentication context and hooks
12
+ export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFederatedAuthEnabled, useTraditionalAuthEnabled } from './AuthContext';
13
+ export type { AuthConfig } from '../types/auth';
@@ -1,131 +1,131 @@
1
- /**
2
- * useAnonSession - React hook for anonymous session management
3
- *
4
- * Provides access to anonymous session preferences stored in Redis.
5
- * Works before user logs in, preferences persist across visits.
6
- */
7
-
8
- 'use client';
9
-
10
- import { useState, useEffect, useCallback } from 'react';
11
-
12
- export interface AnonPreferences {
13
- theme?: string;
14
- locale?: string;
15
- [key: string]: any;
16
- }
17
-
18
- export interface AnonMetrics {
19
- resumeGenerationCount?: number;
20
- firstVisit?: number;
21
- lastVisit?: number;
22
- visitCount?: number;
23
- [key: string]: any;
24
- }
25
-
26
- export interface AnonSession {
27
- id: string;
28
- preferences: AnonPreferences;
29
- metrics: AnonMetrics;
30
- }
31
-
32
- export interface UseAnonSessionReturn {
33
- session: AnonSession | null;
34
- isLoading: boolean;
35
- error: string | null;
36
- updatePreferences: (preferences: Partial<AnonPreferences>) => Promise<void>;
37
- setTheme: (theme: string) => Promise<void>;
38
- refresh: () => Promise<void>;
39
- }
40
-
41
- /**
42
- * Hook to manage anonymous session state
43
- */
44
- export function useAnonSession(): UseAnonSessionReturn {
45
- const [session, setSession] = useState<AnonSession | null>(null);
46
- const [isLoading, setIsLoading] = useState(true);
47
- const [error, setError] = useState<string | null>(null);
48
-
49
- // Fetch session on mount
50
- const fetchSession = useCallback(async () => {
51
- try {
52
- setIsLoading(true);
53
- setError(null);
54
-
55
- const response = await fetch('/api/anon/preferences', {
56
- method: 'GET',
57
- credentials: 'include', // Important for cookies
58
- });
59
-
60
- if (!response.ok) {
61
- throw new Error('Failed to fetch preferences');
62
- }
63
-
64
- const data = await response.json();
65
- if (data.success && data.data) {
66
- setSession({
67
- id: data.data.id,
68
- preferences: data.data.preferences || {},
69
- metrics: data.data.metrics || {},
70
- });
71
- }
72
- } catch (err) {
73
- console.error('[useAnonSession] Error fetching session:', err);
74
- setError(err instanceof Error ? err.message : 'Unknown error');
75
- } finally {
76
- setIsLoading(false);
77
- }
78
- }, []);
79
-
80
- useEffect(() => {
81
- fetchSession();
82
- }, [fetchSession]);
83
-
84
- // Update preferences
85
- const updatePreferences = useCallback(async (preferences: Partial<AnonPreferences>) => {
86
- try {
87
- setError(null);
88
-
89
- const response = await fetch('/api/anon/preferences', {
90
- method: 'POST',
91
- credentials: 'include',
92
- headers: {
93
- 'Content-Type': 'application/json',
94
- },
95
- body: JSON.stringify({ preferences }),
96
- });
97
-
98
- if (!response.ok) {
99
- throw new Error('Failed to update preferences');
100
- }
101
-
102
- const data = await response.json();
103
- if (data.success && data.data) {
104
- setSession(prev => prev ? {
105
- ...prev,
106
- preferences: data.data.preferences,
107
- } : null);
108
- }
109
- } catch (err) {
110
- console.error('[useAnonSession] Error updating preferences:', err);
111
- setError(err instanceof Error ? err.message : 'Unknown error');
112
- throw err;
113
- }
114
- }, []);
115
-
116
- // Convenience method to set theme
117
- const setTheme = useCallback(async (theme: string) => {
118
- await updatePreferences({ theme });
119
- }, [updatePreferences]);
120
-
121
- return {
122
- session,
123
- isLoading,
124
- error,
125
- updatePreferences,
126
- setTheme,
127
- refresh: fetchSession,
128
- };
129
- }
130
-
131
- export default useAnonSession;
1
+ /**
2
+ * useAnonSession - React hook for anonymous session management
3
+ *
4
+ * Provides access to anonymous session preferences stored in Redis.
5
+ * Works before user logs in, preferences persist across visits.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { useState, useEffect, useCallback } from 'react';
11
+
12
+ export interface AnonPreferences {
13
+ theme?: string;
14
+ locale?: string;
15
+ [key: string]: any;
16
+ }
17
+
18
+ export interface AnonMetrics {
19
+ resumeGenerationCount?: number;
20
+ firstVisit?: number;
21
+ lastVisit?: number;
22
+ visitCount?: number;
23
+ [key: string]: any;
24
+ }
25
+
26
+ export interface AnonSession {
27
+ id: string;
28
+ preferences: AnonPreferences;
29
+ metrics: AnonMetrics;
30
+ }
31
+
32
+ export interface UseAnonSessionReturn {
33
+ session: AnonSession | null;
34
+ isLoading: boolean;
35
+ error: string | null;
36
+ updatePreferences: (preferences: Partial<AnonPreferences>) => Promise<void>;
37
+ setTheme: (theme: string) => Promise<void>;
38
+ refresh: () => Promise<void>;
39
+ }
40
+
41
+ /**
42
+ * Hook to manage anonymous session state
43
+ */
44
+ export function useAnonSession(): UseAnonSessionReturn {
45
+ const [session, setSession] = useState<AnonSession | null>(null);
46
+ const [isLoading, setIsLoading] = useState(true);
47
+ const [error, setError] = useState<string | null>(null);
48
+
49
+ // Fetch session on mount
50
+ const fetchSession = useCallback(async () => {
51
+ try {
52
+ setIsLoading(true);
53
+ setError(null);
54
+
55
+ const response = await fetch('/api/anon/preferences', {
56
+ method: 'GET',
57
+ credentials: 'include', // Important for cookies
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error('Failed to fetch preferences');
62
+ }
63
+
64
+ const data = await response.json();
65
+ if (data.success && data.data) {
66
+ setSession({
67
+ id: data.data.id,
68
+ preferences: data.data.preferences || {},
69
+ metrics: data.data.metrics || {},
70
+ });
71
+ }
72
+ } catch (err) {
73
+ console.error('[useAnonSession] Error fetching session:', err);
74
+ setError(err instanceof Error ? err.message : 'Unknown error');
75
+ } finally {
76
+ setIsLoading(false);
77
+ }
78
+ }, []);
79
+
80
+ useEffect(() => {
81
+ fetchSession();
82
+ }, [fetchSession]);
83
+
84
+ // Update preferences
85
+ const updatePreferences = useCallback(async (preferences: Partial<AnonPreferences>) => {
86
+ try {
87
+ setError(null);
88
+
89
+ const response = await fetch('/api/anon/preferences', {
90
+ method: 'POST',
91
+ credentials: 'include',
92
+ headers: {
93
+ 'Content-Type': 'application/json',
94
+ },
95
+ body: JSON.stringify({ preferences }),
96
+ });
97
+
98
+ if (!response.ok) {
99
+ throw new Error('Failed to update preferences');
100
+ }
101
+
102
+ const data = await response.json();
103
+ if (data.success && data.data) {
104
+ setSession(prev => prev ? {
105
+ ...prev,
106
+ preferences: data.data.preferences,
107
+ } : null);
108
+ }
109
+ } catch (err) {
110
+ console.error('[useAnonSession] Error updating preferences:', err);
111
+ setError(err instanceof Error ? err.message : 'Unknown error');
112
+ throw err;
113
+ }
114
+ }, []);
115
+
116
+ // Convenience method to set theme
117
+ const setTheme = useCallback(async (theme: string) => {
118
+ await updatePreferences({ theme });
119
+ }, [updatePreferences]);
120
+
121
+ return {
122
+ session,
123
+ isLoading,
124
+ error,
125
+ updatePreferences,
126
+ setTheme,
127
+ refresh: fetchSession,
128
+ };
129
+ }
130
+
131
+ export default useAnonSession;
@@ -1,87 +1,87 @@
1
- 'use client';
2
-
3
- import React, { createContext, useContext, ReactNode } from 'react';
4
-
5
- // -----------------------------------------------------------------------------
6
- // TYPES
7
- // -----------------------------------------------------------------------------
8
-
9
- export interface VibeAdminConfig {
10
- /** Collection name for this tenant (e.g., "ideal_resume", "idealvibe_app") */
11
- collectionName: string;
12
- /** Display name for the app (e.g., "IdealResume", "IdealVibe") */
13
- appName: string;
14
- /** Optional logo URL */
15
- logoUrl?: string;
16
- /** Base path for admin routes (default: /vibe-admin) */
17
- basePath?: string;
18
- /** Custom tabs to add to the admin panel */
19
- customTabs?: AdminTab[];
20
- /** Theme mode override */
21
- isDarkMode?: boolean;
22
- }
23
-
24
- export interface AdminTab {
25
- id: string;
26
- label: string;
27
- icon?: React.ElementType;
28
- component: React.ComponentType;
29
- }
30
-
31
- export interface VibeAdminContextValue extends VibeAdminConfig {
32
- /** All tabs including defaults and custom */
33
- allTabs: AdminTab[];
34
- }
35
-
36
- // -----------------------------------------------------------------------------
37
- // CONTEXT
38
- // -----------------------------------------------------------------------------
39
-
40
- const VibeAdminContext = createContext<VibeAdminContextValue | null>(null);
41
-
42
- // -----------------------------------------------------------------------------
43
- // HOOK
44
- // -----------------------------------------------------------------------------
45
-
46
- export function useVibeAdmin(): VibeAdminContextValue {
47
- const context = useContext(VibeAdminContext);
48
- if (!context) {
49
- throw new Error('useVibeAdmin must be used within a VibeAdminProvider');
50
- }
51
- return context;
52
- }
53
-
54
- // -----------------------------------------------------------------------------
55
- // PROVIDER
56
- // -----------------------------------------------------------------------------
57
-
58
- interface VibeAdminProviderProps {
59
- config: VibeAdminConfig;
60
- children: ReactNode;
61
- }
62
-
63
- export function VibeAdminProvider({ config, children }: VibeAdminProviderProps) {
64
- // Default tabs - will be populated with actual components
65
- const defaultTabs: AdminTab[] = [
66
- { id: 'stats', label: 'Stats', component: () => null },
67
- { id: 'users', label: 'Users', component: () => null },
68
- { id: 'sessions', label: 'Sessions', component: () => null },
69
- { id: 'analytics', label: 'Analytics', component: () => null },
70
- ];
71
-
72
- const allTabs = [...defaultTabs, ...(config.customTabs || [])];
73
-
74
- const value: VibeAdminContextValue = {
75
- ...config,
76
- basePath: config.basePath || '/vibe-admin',
77
- allTabs,
78
- };
79
-
80
- return (
81
- <VibeAdminContext.Provider value={value}>
82
- {children}
83
- </VibeAdminContext.Provider>
84
- );
85
- }
86
-
87
- export default VibeAdminProvider;
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, ReactNode } from 'react';
4
+
5
+ // -----------------------------------------------------------------------------
6
+ // TYPES
7
+ // -----------------------------------------------------------------------------
8
+
9
+ export interface VibeAdminConfig {
10
+ /** Collection name for this tenant (e.g., "ideal_resume", "idealvibe_app") */
11
+ collectionName: string;
12
+ /** Display name for the app (e.g., "IdealResume", "IdealVibe") */
13
+ appName: string;
14
+ /** Optional logo URL */
15
+ logoUrl?: string;
16
+ /** Base path for admin routes (default: /vibe-admin) */
17
+ basePath?: string;
18
+ /** Custom tabs to add to the admin panel */
19
+ customTabs?: AdminTab[];
20
+ /** Theme mode override */
21
+ isDarkMode?: boolean;
22
+ }
23
+
24
+ export interface AdminTab {
25
+ id: string;
26
+ label: string;
27
+ icon?: React.ElementType;
28
+ component: React.ComponentType;
29
+ }
30
+
31
+ export interface VibeAdminContextValue extends VibeAdminConfig {
32
+ /** All tabs including defaults and custom */
33
+ allTabs: AdminTab[];
34
+ }
35
+
36
+ // -----------------------------------------------------------------------------
37
+ // CONTEXT
38
+ // -----------------------------------------------------------------------------
39
+
40
+ const VibeAdminContext = createContext<VibeAdminContextValue | null>(null);
41
+
42
+ // -----------------------------------------------------------------------------
43
+ // HOOK
44
+ // -----------------------------------------------------------------------------
45
+
46
+ export function useVibeAdmin(): VibeAdminContextValue {
47
+ const context = useContext(VibeAdminContext);
48
+ if (!context) {
49
+ throw new Error('useVibeAdmin must be used within a VibeAdminProvider');
50
+ }
51
+ return context;
52
+ }
53
+
54
+ // -----------------------------------------------------------------------------
55
+ // PROVIDER
56
+ // -----------------------------------------------------------------------------
57
+
58
+ interface VibeAdminProviderProps {
59
+ config: VibeAdminConfig;
60
+ children: ReactNode;
61
+ }
62
+
63
+ export function VibeAdminProvider({ config, children }: VibeAdminProviderProps) {
64
+ // Default tabs - will be populated with actual components
65
+ const defaultTabs: AdminTab[] = [
66
+ { id: 'stats', label: 'Stats', component: () => null },
67
+ { id: 'users', label: 'Users', component: () => null },
68
+ { id: 'sessions', label: 'Sessions', component: () => null },
69
+ { id: 'analytics', label: 'Analytics', component: () => null },
70
+ ];
71
+
72
+ const allTabs = [...defaultTabs, ...(config.customTabs || [])];
73
+
74
+ const value: VibeAdminContextValue = {
75
+ ...config,
76
+ basePath: config.basePath || '/vibe-admin',
77
+ allTabs,
78
+ };
79
+
80
+ return (
81
+ <VibeAdminContext.Provider value={value}>
82
+ {children}
83
+ </VibeAdminContext.Provider>
84
+ );
85
+ }
86
+
87
+ export default VibeAdminProvider;