@payez/next-mvp 3.9.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/api/auth-handler.d.ts +1 -2
  2. package/dist/api/auth-handler.js +9 -9
  3. package/dist/api-handlers/account/change-password.js +110 -112
  4. package/dist/api-handlers/admin/analytics.d.ts +19 -20
  5. package/dist/api-handlers/admin/analytics.js +378 -379
  6. package/dist/api-handlers/admin/audit.d.ts +19 -20
  7. package/dist/api-handlers/admin/audit.js +213 -214
  8. package/dist/api-handlers/admin/index.d.ts +21 -22
  9. package/dist/api-handlers/admin/index.js +42 -43
  10. package/dist/api-handlers/admin/redis-sessions.d.ts +35 -36
  11. package/dist/api-handlers/admin/redis-sessions.js +203 -204
  12. package/dist/api-handlers/admin/sessions.d.ts +20 -21
  13. package/dist/api-handlers/admin/sessions.js +283 -284
  14. package/dist/api-handlers/admin/site-logs.d.ts +45 -46
  15. package/dist/api-handlers/admin/site-logs.js +317 -318
  16. package/dist/api-handlers/admin/stats.d.ts +20 -21
  17. package/dist/api-handlers/admin/stats.js +239 -240
  18. package/dist/api-handlers/admin/users.d.ts +19 -20
  19. package/dist/api-handlers/admin/users.js +221 -222
  20. package/dist/api-handlers/admin/vibe-data.d.ts +79 -80
  21. package/dist/api-handlers/admin/vibe-data.js +267 -268
  22. package/dist/api-handlers/auth/refresh.js +633 -635
  23. package/dist/api-handlers/auth/signout.js +186 -187
  24. package/dist/api-handlers/auth/status.js +4 -7
  25. package/dist/api-handlers/auth/update-session.d.ts +1 -1
  26. package/dist/api-handlers/auth/update-session.js +12 -14
  27. package/dist/api-handlers/auth/verify-code.d.ts +43 -43
  28. package/dist/api-handlers/auth/verify-code.js +90 -94
  29. package/dist/api-handlers/session/viability.js +114 -146
  30. package/dist/api-handlers/test/force-expire.js +59 -65
  31. package/dist/auth/auth-decision.js +182 -182
  32. package/dist/auth/better-auth.d.ts +3 -6
  33. package/dist/auth/better-auth.js +3 -6
  34. package/dist/auth/route-config.js +2 -2
  35. package/dist/auth/utils/token-utils.d.ts +83 -84
  36. package/dist/auth/utils/token-utils.js +218 -219
  37. package/dist/client/AuthContext.js +115 -112
  38. package/dist/client/better-auth-client.d.ts +1020 -1020
  39. package/dist/client/fetch-with-auth.js +2 -2
  40. package/dist/components/SessionSync.js +121 -119
  41. package/dist/components/account/MobileNavDrawer.js +64 -64
  42. package/dist/components/account/UserAvatarMenu.js +91 -88
  43. package/dist/components/admin/VibeAdminLayout.js +71 -69
  44. package/dist/hooks/useAuth.js +9 -7
  45. package/dist/hooks/useAuthSettings.js +93 -93
  46. package/dist/hooks/useAvailableProviders.d.ts +43 -45
  47. package/dist/hooks/useAvailableProviders.js +112 -108
  48. package/dist/hooks/useSessionExpiration.d.ts +2 -3
  49. package/dist/hooks/useSessionExpiration.js +2 -2
  50. package/dist/hooks/useViabilitySession.js +3 -2
  51. package/dist/index.js +4 -6
  52. package/dist/lib/app-slug.d.ts +95 -95
  53. package/dist/lib/app-slug.js +172 -172
  54. package/dist/lib/standardized-client-api.js +10 -5
  55. package/dist/lib/startup-init.js +21 -25
  56. package/dist/lib/test-aware-get-token.js +86 -81
  57. package/dist/lib/token-lifecycle.d.ts +78 -52
  58. package/dist/lib/token-lifecycle.js +360 -398
  59. package/dist/pages/admin-login/page.js +73 -83
  60. package/dist/pages/client-admin/ClientSiteAdminPage.js +179 -177
  61. package/dist/pages/login/page.js +202 -211
  62. package/dist/pages/showcase/ShowcasePage.js +142 -140
  63. package/dist/pages/test-env/EmergencyLogoutPage.js +99 -98
  64. package/dist/pages/test-env/JwtInspectPage.js +116 -114
  65. package/dist/pages/test-env/RefreshTokenPage.js +4 -2
  66. package/dist/pages/test-env/TestEnvPage.js +51 -49
  67. package/dist/pages/verify-code/page.js +412 -408
  68. package/dist/routes/auth/logout.d.ts +31 -31
  69. package/dist/routes/auth/logout.js +98 -113
  70. package/dist/routes/auth/nextauth.d.ts +14 -11
  71. package/dist/routes/auth/nextauth.js +25 -57
  72. package/dist/routes/auth/session.js +157 -179
  73. package/dist/routes/auth/viability.js +190 -201
  74. package/dist/server/auth.d.ts +50 -0
  75. package/dist/server/auth.js +62 -0
  76. package/dist/stores/authStore.js +19 -23
  77. package/dist/utils/logout.js +5 -5
  78. package/package.json +1 -3
  79. package/src/api/auth-handler.ts +550 -549
  80. package/src/api-handlers/account/change-password.ts +5 -8
  81. package/src/api-handlers/admin/analytics.ts +4 -6
  82. package/src/api-handlers/admin/audit.ts +5 -7
  83. package/src/api-handlers/admin/index.ts +1 -2
  84. package/src/api-handlers/admin/redis-sessions.ts +6 -8
  85. package/src/api-handlers/admin/sessions.ts +5 -7
  86. package/src/api-handlers/admin/site-logs.ts +8 -10
  87. package/src/api-handlers/admin/stats.ts +4 -6
  88. package/src/api-handlers/admin/users.ts +5 -7
  89. package/src/api-handlers/admin/vibe-data.ts +10 -12
  90. package/src/api-handlers/auth/refresh.ts +5 -7
  91. package/src/api-handlers/auth/signout.ts +5 -6
  92. package/src/api-handlers/auth/status.ts +4 -7
  93. package/src/api-handlers/auth/update-session.ts +123 -125
  94. package/src/api-handlers/auth/verify-code.ts +9 -13
  95. package/src/api-handlers/session/viability.ts +10 -47
  96. package/src/api-handlers/test/force-expire.ts +4 -11
  97. package/src/auth/auth-decision.ts +1 -1
  98. package/src/auth/better-auth.ts +138 -141
  99. package/src/auth/route-config.ts +219 -219
  100. package/src/auth/utils/token-utils.ts +0 -1
  101. package/src/client/AuthContext.tsx +6 -2
  102. package/src/client/fetch-with-auth.ts +47 -47
  103. package/src/components/SessionSync.tsx +6 -5
  104. package/src/components/account/MobileNavDrawer.tsx +3 -3
  105. package/src/components/account/UserAvatarMenu.tsx +6 -3
  106. package/src/components/admin/VibeAdminLayout.tsx +4 -2
  107. package/src/config/logger.ts +1 -1
  108. package/src/hooks/useAuth.ts +117 -115
  109. package/src/hooks/useAuthSettings.ts +2 -2
  110. package/src/hooks/useAvailableProviders.ts +9 -5
  111. package/src/hooks/useSessionExpiration.ts +101 -102
  112. package/src/hooks/useViabilitySession.ts +336 -335
  113. package/src/index.ts +60 -63
  114. package/src/lib/api-handler.ts +0 -1
  115. package/src/lib/app-slug.ts +6 -6
  116. package/src/lib/standardized-client-api.ts +901 -895
  117. package/src/lib/startup-init.ts +243 -247
  118. package/src/lib/test-aware-get-token.ts +22 -12
  119. package/src/lib/token-lifecycle.ts +12 -53
  120. package/src/pages/admin-login/page.tsx +9 -17
  121. package/src/pages/client-admin/ClientSiteAdminPage.tsx +4 -2
  122. package/src/pages/login/page.tsx +21 -28
  123. package/src/pages/showcase/ShowcasePage.tsx +4 -2
  124. package/src/pages/test-env/EmergencyLogoutPage.tsx +7 -6
  125. package/src/pages/test-env/JwtInspectPage.tsx +5 -3
  126. package/src/pages/test-env/RefreshTokenPage.tsx +157 -155
  127. package/src/pages/test-env/TestEnvPage.tsx +4 -2
  128. package/src/pages/verify-code/page.tsx +10 -6
  129. package/src/routes/auth/logout.ts +7 -25
  130. package/src/routes/auth/nextauth.ts +45 -71
  131. package/src/routes/auth/session.ts +25 -50
  132. package/src/routes/auth/viability.ts +7 -19
  133. package/src/server/auth.ts +60 -0
  134. package/src/stores/authStore.ts +1899 -1904
  135. package/src/utils/logout.ts +30 -30
  136. package/src/auth/auth-options.ts +0 -237
  137. package/src/auth/callbacks/index.ts +0 -7
  138. package/src/auth/callbacks/jwt.ts +0 -382
  139. package/src/auth/callbacks/session.ts +0 -243
  140. package/src/auth/callbacks/signin.ts +0 -56
  141. package/src/auth/events/index.ts +0 -5
  142. package/src/auth/events/signout.ts +0 -33
  143. package/src/auth/providers/credentials.ts +0 -256
  144. package/src/auth/providers/index.ts +0 -6
  145. package/src/auth/providers/oauth.ts +0 -114
  146. package/src/lib/nextauth-secret.ts +0 -121
  147. package/src/types/next-auth.d.ts +0 -15
@@ -1,83 +1,73 @@
1
- "use strict";
2
- /**
3
- * Admin Login Page for @payez/next-mvp
4
- *
5
- * A standalone username/password login page for admin access.
6
- * NOT linked from any navigation - only accessible via direct URL.
7
- *
8
- * USAGE:
9
- * 1. Create app/account-auth/admin-login/page.tsx in your Next.js app
10
- * 2. Re-export this component:
11
- * export { default } from '@payez/next-mvp/pages/admin-login';
12
- *
13
- * CUSTOMIZATION:
14
- * - Override styles via CSS variables or wrap with your own component
15
- * - Provide custom branding via ThemeProvider
16
- */
17
- 'use client';
18
- Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.default = AdminLoginPage;
20
- exports.AdminLoginForm = AdminLoginForm;
21
- exports.AdminLoginFallback = AdminLoginFallback;
22
- const jsx_runtime_1 = require("react/jsx-runtime");
23
- const react_1 = require("react");
24
- const react_2 = require("next-auth/react");
25
- const navigation_1 = require("next/navigation");
26
- const react_3 = require("react");
27
- const useTheme_1 = require("../../theme/useTheme");
28
- function AdminLoginForm({ title = 'Admin Login', subtitle = 'Authorized personnel only', callbackUrl: propCallbackUrl, logo, }) {
29
- const searchParams = (0, navigation_1.useSearchParams)();
30
- const callbackUrl = propCallbackUrl || searchParams?.get('callbackUrl') || '/dashboard';
31
- const branding = (0, useTheme_1.useBranding)();
32
- const colors = (0, useTheme_1.useColors)();
33
- const [email, setEmail] = (0, react_1.useState)('');
34
- const [password, setPassword] = (0, react_1.useState)('');
35
- const [showPassword, setShowPassword] = (0, react_1.useState)(false);
36
- const [isLoading, setIsLoading] = (0, react_1.useState)(false);
37
- const [error, setError] = (0, react_1.useState)(null);
38
- const handleSubmit = async (e) => {
39
- e.preventDefault();
40
- setIsLoading(true);
41
- setError(null);
42
- try {
43
- const result = await (0, react_2.signIn)('credentials', {
44
- email,
45
- password,
46
- redirect: false,
47
- callbackUrl,
48
- });
49
- if (result?.error) {
50
- // Parse structured error if available
51
- try {
52
- const errorData = JSON.parse(result.error);
53
- setError(errorData.message || errorData.error?.message || 'Invalid credentials');
54
- }
55
- catch {
56
- if (result.error === 'CredentialsSignin') {
57
- setError('Invalid email or password');
58
- }
59
- else {
60
- setError(result.error);
61
- }
62
- }
63
- }
64
- else if (result?.ok) {
65
- // Redirect to verify-code for 2FA or directly to callback
66
- window.location.href = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
67
- }
68
- }
69
- catch (err) {
70
- setError('An unexpected error occurred');
71
- }
72
- finally {
73
- setIsLoading(false);
74
- }
75
- };
76
- return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900 p-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full", children: [logo && ((0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: logo })), (0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold text-center mb-2 text-slate-900", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "text-center mb-8 text-slate-600", children: subtitle }), error && ((0, jsx_runtime_1.jsx)("div", { className: "mb-6 px-4 py-3 rounded-lg bg-red-500 text-white text-center text-sm", children: error })), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "admin-email", className: "block text-sm font-medium mb-2 text-slate-700", children: "Email" }), (0, jsx_runtime_1.jsx)("input", { id: "admin-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: isLoading, autoComplete: "email", className: "w-full px-4 py-3 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed", placeholder: "admin@example.com" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "admin-password", className: "block text-sm font-medium mb-2 text-slate-700", children: "Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { id: "admin-password", type: showPassword ? 'text' : 'password', value: password, onChange: (e) => setPassword(e.target.value), required: true, disabled: isLoading, autoComplete: "current-password", className: "w-full px-4 py-3 pr-12 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed", placeholder: "Enter your password" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-500 hover:text-slate-700", "aria-label": showPassword ? 'Hide password' : 'Show password', children: showPassword ? ((0, jsx_runtime_1.jsx)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L6.464 6.464m7.535 7.535l3.415 3.414M3 3l3.464 3.464M21 21l-3.415-3.414" }) })) : ((0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: [(0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }), (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })] })) })] })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: isLoading, className: "w-full py-3 px-4 rounded-lg font-semibold text-white transition-colors bg-slate-700 hover:bg-slate-800 disabled:bg-slate-400 disabled:cursor-not-allowed", children: isLoading ? ((0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin -ml-1 mr-3 h-5 w-5 text-white", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Signing in..."] })) : ('Sign In') })] }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-6 text-center text-xs text-slate-500", children: ["This login is for authorized administrators only.", (0, jsx_runtime_1.jsx)("br", {}), "All access attempts are logged."] })] }) }));
77
- }
78
- function AdminLoginFallback() {
79
- return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-10 w-10 mx-auto text-white", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), (0, jsx_runtime_1.jsx)("p", { className: "mt-4 text-slate-400", children: "Loading..." })] }) }));
80
- }
81
- function AdminLoginPage(props) {
82
- return ((0, jsx_runtime_1.jsx)(react_3.Suspense, { fallback: (0, jsx_runtime_1.jsx)(AdminLoginFallback, {}), children: (0, jsx_runtime_1.jsx)(AdminLoginForm, { ...props }) }));
83
- }
1
+ "use strict";
2
+ /**
3
+ * Admin Login Page for @payez/next-mvp
4
+ *
5
+ * A standalone username/password login page for admin access.
6
+ * NOT linked from any navigation - only accessible via direct URL.
7
+ *
8
+ * USAGE:
9
+ * 1. Create app/account-auth/admin-login/page.tsx in your Next.js app
10
+ * 2. Re-export this component:
11
+ * export { default } from '@payez/next-mvp/pages/admin-login';
12
+ *
13
+ * CUSTOMIZATION:
14
+ * - Override styles via CSS variables or wrap with your own component
15
+ * - Provide custom branding via ThemeProvider
16
+ */
17
+ 'use client';
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.default = AdminLoginPage;
20
+ exports.AdminLoginForm = AdminLoginForm;
21
+ exports.AdminLoginFallback = AdminLoginFallback;
22
+ const jsx_runtime_1 = require("react/jsx-runtime");
23
+ const react_1 = require("react");
24
+ const better_auth_client_1 = require("../../client/better-auth-client");
25
+ const navigation_1 = require("next/navigation");
26
+ const react_2 = require("react");
27
+ const useTheme_1 = require("../../theme/useTheme");
28
+ function AdminLoginForm({ title = 'Admin Login', subtitle = 'Authorized personnel only', callbackUrl: propCallbackUrl, logo, }) {
29
+ const searchParams = (0, navigation_1.useSearchParams)();
30
+ const callbackUrl = propCallbackUrl || searchParams?.get('callbackUrl') || '/dashboard';
31
+ const branding = (0, useTheme_1.useBranding)();
32
+ const colors = (0, useTheme_1.useColors)();
33
+ const [email, setEmail] = (0, react_1.useState)('');
34
+ const [password, setPassword] = (0, react_1.useState)('');
35
+ const [showPassword, setShowPassword] = (0, react_1.useState)(false);
36
+ const [isLoading, setIsLoading] = (0, react_1.useState)(false);
37
+ const [error, setError] = (0, react_1.useState)(null);
38
+ const handleSubmit = async (e) => {
39
+ e.preventDefault();
40
+ setIsLoading(true);
41
+ setError(null);
42
+ try {
43
+ const result = await better_auth_client_1.authClient.signIn.email({
44
+ email,
45
+ password,
46
+ callbackURL: callbackUrl,
47
+ });
48
+ if (result?.error) {
49
+ const errorMsg = typeof result.error === 'object'
50
+ ? result.error.message || 'Invalid credentials'
51
+ : String(result.error);
52
+ setError(errorMsg);
53
+ }
54
+ else if (result?.data) {
55
+ // Redirect to verify-code for 2FA or directly to callback
56
+ window.location.href = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
57
+ }
58
+ }
59
+ catch (err) {
60
+ setError('An unexpected error occurred');
61
+ }
62
+ finally {
63
+ setIsLoading(false);
64
+ }
65
+ };
66
+ return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900 p-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full", children: [logo && ((0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: logo })), (0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold text-center mb-2 text-slate-900", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "text-center mb-8 text-slate-600", children: subtitle }), error && ((0, jsx_runtime_1.jsx)("div", { className: "mb-6 px-4 py-3 rounded-lg bg-red-500 text-white text-center text-sm", children: error })), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "admin-email", className: "block text-sm font-medium mb-2 text-slate-700", children: "Email" }), (0, jsx_runtime_1.jsx)("input", { id: "admin-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: isLoading, autoComplete: "email", className: "w-full px-4 py-3 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed", placeholder: "admin@example.com" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "admin-password", className: "block text-sm font-medium mb-2 text-slate-700", children: "Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { id: "admin-password", type: showPassword ? 'text' : 'password', value: password, onChange: (e) => setPassword(e.target.value), required: true, disabled: isLoading, autoComplete: "current-password", className: "w-full px-4 py-3 pr-12 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed", placeholder: "Enter your password" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-500 hover:text-slate-700", "aria-label": showPassword ? 'Hide password' : 'Show password', children: showPassword ? ((0, jsx_runtime_1.jsx)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L6.464 6.464m7.535 7.535l3.415 3.414M3 3l3.464 3.464M21 21l-3.415-3.414" }) })) : ((0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: [(0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }), (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })] })) })] })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: isLoading, className: "w-full py-3 px-4 rounded-lg font-semibold text-white transition-colors bg-slate-700 hover:bg-slate-800 disabled:bg-slate-400 disabled:cursor-not-allowed", children: isLoading ? ((0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin -ml-1 mr-3 h-5 w-5 text-white", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Signing in..."] })) : ('Sign In') })] }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-6 text-center text-xs text-slate-500", children: ["This login is for authorized administrators only.", (0, jsx_runtime_1.jsx)("br", {}), "All access attempts are logged."] })] }) }));
67
+ }
68
+ function AdminLoginFallback() {
69
+ return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-10 w-10 mx-auto text-white", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), (0, jsx_runtime_1.jsx)("p", { className: "mt-4 text-slate-400", children: "Loading..." })] }) }));
70
+ }
71
+ function AdminLoginPage(props) {
72
+ return ((0, jsx_runtime_1.jsx)(react_2.Suspense, { fallback: (0, jsx_runtime_1.jsx)(AdminLoginFallback, {}), children: (0, jsx_runtime_1.jsx)(AdminLoginForm, { ...props }) }));
73
+ }
@@ -1,177 +1,179 @@
1
- "use strict";
2
- 'use client';
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
- Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.ClientSiteAdminPage = ClientSiteAdminPage;
8
- const jsx_runtime_1 = require("react/jsx-runtime");
9
- /**
10
- * =============================================================================
11
- * CLIENT SITE ADMIN - Your App's Admin Dashboard
12
- * =============================================================================
13
- *
14
- * This is the foundation for YOUR app-specific admin area.
15
- * The MVP admin (/admin) handles membership - this handles YOUR data.
16
- *
17
- * SEPARATION OF CONCERNS:
18
- * ┌─────────────────────────────────────────────────────────────────────┐
19
- * │ /admin (MVP Admin) │ /admin/[your-area] (This) │
20
- * │ ───────────────── │ ──────────────────────── │
21
- * │ • User authentication │ • Your app's collections │
22
- * │ • Sessions management │ • Your app's features │
23
- * │ • Role assignments │ • Content moderation │
24
- * │ • Tier/subscription mgmt │ • App-specific analytics │
25
- * │ • Site-wide analytics │ • Custom admin tools │
26
- * │ • vibe_app collection │ • your_app collection │
27
- * └─────────────────────────────────────────────────────────────────────┘
28
- *
29
- * USAGE:
30
- * ```tsx
31
- * // app/admin/your-area/page.tsx
32
- * import { ClientSiteAdminPage } from '@payez/next-mvp/pages/client-admin';
33
- *
34
- * export default function YourAdminPage() {
35
- * return (
36
- * <ClientSiteAdminPage
37
- * title="Your Admin"
38
- * subtitle="Manage your app data"
39
- * collectionName="your_collection"
40
- * tabs={[
41
- * { id: 'overview', label: 'Overview', icon: '📊' },
42
- * { id: 'items', label: 'Items', icon: '📦', table: 'items' },
43
- * ]}
44
- * renderTabContent={(tab, data) => <YourCustomContent />}
45
- * />
46
- * );
47
- * }
48
- * ```
49
- *
50
- * =============================================================================
51
- */
52
- const react_1 = require("react");
53
- const react_2 = require("next-auth/react");
54
- const navigation_1 = require("next/navigation");
55
- const link_1 = __importDefault(require("next/link"));
56
- // Icons - using basic SVGs to avoid lucide dependency issues
57
- const IconRefresh = () => ((0, jsx_runtime_1.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }));
58
- const IconArrowLeft = () => ((0, jsx_runtime_1.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 19l-7-7m0 0l7-7m-7 7h18" }) }));
59
- const IconShield = () => ((0, jsx_runtime_1.jsx)("svg", { className: "w-16 h-16", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" }) }));
60
- const IconChart = ({ className = "w-5 h-5" }) => ((0, jsx_runtime_1.jsx)("svg", { className: className, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }));
61
- function ClientSiteAdminPage({ title, subtitle = 'Manage your app data', collectionName, tabs, allowedRoles = ['vibe_app_admin'], renderOverview, renderTabContent, isDark: isDarkProp, backUrl = '/', backLabel = 'Back to Site', }) {
62
- const { data: session, status } = (0, react_2.useSession)();
63
- const router = (0, navigation_1.useRouter)();
64
- // Theme detection
65
- const [isDarkState, setIsDarkState] = (0, react_1.useState)(false);
66
- (0, react_1.useEffect)(() => {
67
- if (isDarkProp === undefined) {
68
- const checkDark = () => {
69
- const isDark = document.documentElement.classList.contains('dark') ||
70
- window.matchMedia('(prefers-color-scheme: dark)').matches;
71
- setIsDarkState(isDark);
72
- };
73
- checkDark();
74
- const mq = window.matchMedia('(prefers-color-scheme: dark)');
75
- mq.addEventListener('change', checkDark);
76
- return () => mq.removeEventListener('change', checkDark);
77
- }
78
- }, [isDarkProp]);
79
- const isDark = isDarkProp ?? isDarkState;
80
- const [activeTab, setActiveTab] = (0, react_1.useState)(tabs[0]?.id || 'overview');
81
- const [tableData, setTableData] = (0, react_1.useState)([]);
82
- const [loading, setLoading] = (0, react_1.useState)(false);
83
- const [stats, setStats] = (0, react_1.useState)([]);
84
- const [statsLoading, setStatsLoading] = (0, react_1.useState)(false);
85
- const userRoles = session?.user?.roles || [];
86
- const hasAccess = allowedRoles.some(role => userRoles.includes(role));
87
- const themeClasses = {
88
- bg: isDark ? 'bg-slate-950' : 'bg-gray-50',
89
- cardBg: isDark ? 'bg-slate-800 border-slate-700' : 'bg-white border-gray-200',
90
- headerBg: isDark ? 'bg-slate-900 border-slate-800' : 'bg-white border-gray-200',
91
- text: isDark ? 'text-white' : 'text-gray-900',
92
- textMuted: isDark ? 'text-slate-400' : 'text-gray-600',
93
- tabBg: isDark ? 'bg-slate-800' : 'bg-gray-100',
94
- tabActive: 'bg-amber-600 text-white shadow-lg',
95
- tabInactive: isDark ? 'text-slate-400 hover:bg-slate-700' : 'text-gray-600 hover:bg-gray-200',
96
- };
97
- // Fetch collection stats
98
- const fetchStats = (0, react_1.useCallback)(async () => {
99
- setStatsLoading(true);
100
- try {
101
- const res = await fetch(`/api/vibe/collections/${collectionName}/tables`);
102
- if (res.ok) {
103
- const data = await res.json();
104
- const tables = data.tables || data || [];
105
- setStats(tables.map((tbl) => ({
106
- table: typeof tbl === 'string' ? tbl : tbl.name,
107
- count: typeof tbl === 'object' ? (tbl.document_count || tbl.count || 0) : 0,
108
- label: (typeof tbl === 'string' ? tbl : tbl.name)
109
- .replace(/_/g, ' ')
110
- .replace(/\b\w/g, (c) => c.toUpperCase()),
111
- })));
112
- }
113
- }
114
- catch (err) {
115
- console.error('Failed to fetch stats:', err);
116
- }
117
- finally {
118
- setStatsLoading(false);
119
- }
120
- }, [collectionName]);
121
- // Fetch table data
122
- const fetchTableData = (0, react_1.useCallback)(async (tableName) => {
123
- setLoading(true);
124
- try {
125
- const res = await fetch(`/api/vibe/data/${collectionName}/${tableName}?limit=100`);
126
- if (res.ok) {
127
- const data = await res.json();
128
- setTableData(data.data || []);
129
- }
130
- }
131
- catch (err) {
132
- console.error('Failed to fetch table:', err);
133
- setTableData([]);
134
- }
135
- finally {
136
- setLoading(false);
137
- }
138
- }, [collectionName]);
139
- // Handle tab change
140
- const handleTabChange = (0, react_1.useCallback)((tabId) => {
141
- setActiveTab(tabId);
142
- const tab = tabs.find(t => t.id === tabId);
143
- if (tab?.table) {
144
- fetchTableData(tab.table);
145
- }
146
- }, [tabs, fetchTableData]);
147
- // Initial load
148
- (0, react_1.useEffect)(() => {
149
- if (status === 'unauthenticated') {
150
- router.push(`/account-auth/login?callbackUrl=${encodeURIComponent(window.location.pathname)}`);
151
- }
152
- else if (status === 'authenticated' && !hasAccess) {
153
- router.push('/?error=unauthorized');
154
- }
155
- else if (status === 'authenticated' && hasAccess) {
156
- fetchStats();
157
- }
158
- }, [status, hasAccess, router, fetchStats]);
159
- // Loading state
160
- if (status === 'loading') {
161
- return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center ${themeClasses.bg}`, children: (0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-12 w-12 border-b-2 border-amber-500" }) }));
162
- }
163
- // Access denied
164
- if (status === 'authenticated' && !hasAccess) {
165
- return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center ${themeClasses.bg}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: isDark ? 'text-red-400' : 'text-red-500', children: (0, jsx_runtime_1.jsx)(IconShield, {}) }), (0, jsx_runtime_1.jsx)("h1", { className: `text-2xl font-bold mb-2 ${themeClasses.text}`, children: "Access Denied" }), (0, jsx_runtime_1.jsx)("p", { className: `mb-6 ${themeClasses.textMuted}`, children: "You need admin access for this area." }), (0, jsx_runtime_1.jsxs)(link_1.default, { href: "/", className: "inline-flex items-center gap-2 px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700", children: [(0, jsx_runtime_1.jsx)(IconArrowLeft, {}), " Return to Home"] })] }) }));
166
- }
167
- if (status === 'unauthenticated')
168
- return null;
169
- const currentTab = tabs.find(t => t.id === activeTab);
170
- return ((0, jsx_runtime_1.jsxs)("div", { className: `min-h-screen ${themeClasses.bg}`, children: [(0, jsx_runtime_1.jsx)("header", { className: `sticky top-0 z-40 border-b ${themeClasses.headerBg}`, children: (0, jsx_runtime_1.jsx)("div", { className: "container mx-auto px-4 py-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: `p-2 rounded-lg ${isDark ? 'bg-amber-900/30' : 'bg-amber-100'}`, children: (0, jsx_runtime_1.jsx)(IconChart, { className: `w-5 h-5 ${isDark ? 'text-amber-400' : 'text-amber-600'}` }) }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: `text-xl font-bold ${themeClasses.text}`, children: title }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted}`, children: subtitle })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsxs)("button", { onClick: () => { fetchStats(); if (currentTab?.table)
171
- fetchTableData(currentTab.table); }, disabled: loading || statsLoading, className: `flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${themeClasses.textMuted} ${isDark ? 'hover:bg-slate-800' : 'hover:bg-gray-100'} ${(loading || statsLoading) ? 'opacity-50' : ''}`, children: [(0, jsx_runtime_1.jsx)("span", { className: (loading || statsLoading) ? 'animate-spin' : '', children: (0, jsx_runtime_1.jsx)(IconRefresh, {}) }), "Refresh"] }), (0, jsx_runtime_1.jsxs)(link_1.default, { href: backUrl, className: `flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${themeClasses.textMuted} ${isDark ? 'hover:bg-slate-800' : 'hover:bg-gray-100'}`, children: [(0, jsx_runtime_1.jsx)(IconArrowLeft, {}), " ", backLabel] })] })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "container mx-auto px-4 py-6 space-y-6", children: [(0, jsx_runtime_1.jsx)("div", { className: `flex flex-wrap gap-1 p-1 rounded-xl ${themeClasses.tabBg}`, children: tabs.map((tab) => ((0, jsx_runtime_1.jsxs)("button", { onClick: () => handleTabChange(tab.id), className: `py-2 px-4 rounded-lg text-sm font-medium transition-all ${activeTab === tab.id ? themeClasses.tabActive : themeClasses.tabInactive}`, children: [(0, jsx_runtime_1.jsx)("span", { className: "mr-2", children: tab.icon }), tab.label] }, tab.id))) }), renderTabContent ? (renderTabContent(currentTab, tableData, loading)) : activeTab === 'overview' || !currentTab?.table ? (
172
- // Default Overview
173
- (0, jsx_runtime_1.jsxs)("div", { className: "space-y-6", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h2", { className: `text-lg font-semibold mb-4 ${themeClasses.text}`, children: "Data Overview" }), (0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4", children: stats.map((stat) => ((0, jsx_runtime_1.jsxs)(link_1.default, { href: `/admin/data?collection=${collectionName}&table=${stat.table}`, className: `p-4 rounded-xl border transition-all hover:scale-[1.02] ${themeClasses.cardBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-xs font-medium ${themeClasses.textMuted}`, children: stat.label }), (0, jsx_runtime_1.jsx)("p", { className: `text-2xl font-bold ${themeClasses.text}`, children: statsLoading ? '...' : stat.count.toLocaleString() })] }, stat.table))) })] }), renderOverview && renderOverview(stats)] })) : (
174
- // Default Table View
175
- (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("h2", { className: `text-lg font-semibold mb-4 ${themeClasses.text}`, children: [currentTab.label, " (", tableData.length, ")"] }), loading ? ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center py-12", children: (0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500" }) })) : ((0, jsx_runtime_1.jsx)("div", { className: `rounded-lg border overflow-hidden ${themeClasses.cardBg}`, children: (0, jsx_runtime_1.jsx)("div", { className: "overflow-x-auto", children: (0, jsx_runtime_1.jsxs)("table", { className: "w-full text-sm", children: [(0, jsx_runtime_1.jsx)("thead", { className: isDark ? 'bg-slate-800' : 'bg-gray-50', children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "ID" }), (0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "User" }), (0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "Data" }), (0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "Created" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { className: `divide-y ${isDark ? 'divide-slate-700' : 'divide-gray-200'}`, children: tableData.length === 0 ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: 4, className: `px-4 py-8 text-center ${themeClasses.textMuted}`, children: "No data found" }) })) : tableData.map((row) => ((0, jsx_runtime_1.jsxs)("tr", { className: isDark ? 'hover:bg-slate-800/50' : 'hover:bg-gray-50', children: [(0, jsx_runtime_1.jsx)("td", { className: `px-4 py-3 ${themeClasses.text}`, children: row.document_id }), (0, jsx_runtime_1.jsx)("td", { className: `px-4 py-3 ${themeClasses.text}`, children: row.user_id }), (0, jsx_runtime_1.jsxs)("td", { className: `px-4 py-3 ${themeClasses.textMuted} max-w-xs truncate`, children: [JSON.stringify(row.data).substring(0, 50), "..."] }), (0, jsx_runtime_1.jsx)("td", { className: `px-4 py-3 ${themeClasses.textMuted}`, children: new Date(row.created_at).toLocaleDateString() })] }, row.document_id))) })] }) }) }))] }))] })] }));
176
- }
177
- exports.default = ClientSiteAdminPage;
1
+ "use strict";
2
+ 'use client';
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ClientSiteAdminPage = ClientSiteAdminPage;
8
+ const jsx_runtime_1 = require("react/jsx-runtime");
9
+ /**
10
+ * =============================================================================
11
+ * CLIENT SITE ADMIN - Your App's Admin Dashboard
12
+ * =============================================================================
13
+ *
14
+ * This is the foundation for YOUR app-specific admin area.
15
+ * The MVP admin (/admin) handles membership - this handles YOUR data.
16
+ *
17
+ * SEPARATION OF CONCERNS:
18
+ * ┌─────────────────────────────────────────────────────────────────────┐
19
+ * │ /admin (MVP Admin) │ /admin/[your-area] (This) │
20
+ * │ ───────────────── │ ──────────────────────── │
21
+ * │ • User authentication │ • Your app's collections │
22
+ * │ • Sessions management │ • Your app's features │
23
+ * │ • Role assignments │ • Content moderation │
24
+ * │ • Tier/subscription mgmt │ • App-specific analytics │
25
+ * │ • Site-wide analytics │ • Custom admin tools │
26
+ * │ • vibe_app collection │ • your_app collection │
27
+ * └─────────────────────────────────────────────────────────────────────┘
28
+ *
29
+ * USAGE:
30
+ * ```tsx
31
+ * // app/admin/your-area/page.tsx
32
+ * import { ClientSiteAdminPage } from '@payez/next-mvp/pages/client-admin';
33
+ *
34
+ * export default function YourAdminPage() {
35
+ * return (
36
+ * <ClientSiteAdminPage
37
+ * title="Your Admin"
38
+ * subtitle="Manage your app data"
39
+ * collectionName="your_collection"
40
+ * tabs={[
41
+ * { id: 'overview', label: 'Overview', icon: '📊' },
42
+ * { id: 'items', label: 'Items', icon: '📦', table: 'items' },
43
+ * ]}
44
+ * renderTabContent={(tab, data) => <YourCustomContent />}
45
+ * />
46
+ * );
47
+ * }
48
+ * ```
49
+ *
50
+ * =============================================================================
51
+ */
52
+ const react_1 = require("react");
53
+ const better_auth_client_1 = require("../../client/better-auth-client");
54
+ const navigation_1 = require("next/navigation");
55
+ const link_1 = __importDefault(require("next/link"));
56
+ // Icons - using basic SVGs to avoid lucide dependency issues
57
+ const IconRefresh = () => ((0, jsx_runtime_1.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }));
58
+ const IconArrowLeft = () => ((0, jsx_runtime_1.jsx)("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 19l-7-7m0 0l7-7m-7 7h18" }) }));
59
+ const IconShield = () => ((0, jsx_runtime_1.jsx)("svg", { className: "w-16 h-16", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" }) }));
60
+ const IconChart = ({ className = "w-5 h-5" }) => ((0, jsx_runtime_1.jsx)("svg", { className: className, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }));
61
+ function ClientSiteAdminPage({ title, subtitle = 'Manage your app data', collectionName, tabs, allowedRoles = ['vibe_app_admin'], renderOverview, renderTabContent, isDark: isDarkProp, backUrl = '/', backLabel = 'Back to Site', }) {
62
+ const { data: sessionData, isPending } = better_auth_client_1.authClient.useSession();
63
+ const session = sessionData;
64
+ const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
65
+ const router = (0, navigation_1.useRouter)();
66
+ // Theme detection
67
+ const [isDarkState, setIsDarkState] = (0, react_1.useState)(false);
68
+ (0, react_1.useEffect)(() => {
69
+ if (isDarkProp === undefined) {
70
+ const checkDark = () => {
71
+ const isDark = document.documentElement.classList.contains('dark') ||
72
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
73
+ setIsDarkState(isDark);
74
+ };
75
+ checkDark();
76
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
77
+ mq.addEventListener('change', checkDark);
78
+ return () => mq.removeEventListener('change', checkDark);
79
+ }
80
+ }, [isDarkProp]);
81
+ const isDark = isDarkProp ?? isDarkState;
82
+ const [activeTab, setActiveTab] = (0, react_1.useState)(tabs[0]?.id || 'overview');
83
+ const [tableData, setTableData] = (0, react_1.useState)([]);
84
+ const [loading, setLoading] = (0, react_1.useState)(false);
85
+ const [stats, setStats] = (0, react_1.useState)([]);
86
+ const [statsLoading, setStatsLoading] = (0, react_1.useState)(false);
87
+ const userRoles = session?.user?.roles || [];
88
+ const hasAccess = allowedRoles.some(role => userRoles.includes(role));
89
+ const themeClasses = {
90
+ bg: isDark ? 'bg-slate-950' : 'bg-gray-50',
91
+ cardBg: isDark ? 'bg-slate-800 border-slate-700' : 'bg-white border-gray-200',
92
+ headerBg: isDark ? 'bg-slate-900 border-slate-800' : 'bg-white border-gray-200',
93
+ text: isDark ? 'text-white' : 'text-gray-900',
94
+ textMuted: isDark ? 'text-slate-400' : 'text-gray-600',
95
+ tabBg: isDark ? 'bg-slate-800' : 'bg-gray-100',
96
+ tabActive: 'bg-amber-600 text-white shadow-lg',
97
+ tabInactive: isDark ? 'text-slate-400 hover:bg-slate-700' : 'text-gray-600 hover:bg-gray-200',
98
+ };
99
+ // Fetch collection stats
100
+ const fetchStats = (0, react_1.useCallback)(async () => {
101
+ setStatsLoading(true);
102
+ try {
103
+ const res = await fetch(`/api/vibe/collections/${collectionName}/tables`);
104
+ if (res.ok) {
105
+ const data = await res.json();
106
+ const tables = data.tables || data || [];
107
+ setStats(tables.map((tbl) => ({
108
+ table: typeof tbl === 'string' ? tbl : tbl.name,
109
+ count: typeof tbl === 'object' ? (tbl.document_count || tbl.count || 0) : 0,
110
+ label: (typeof tbl === 'string' ? tbl : tbl.name)
111
+ .replace(/_/g, ' ')
112
+ .replace(/\b\w/g, (c) => c.toUpperCase()),
113
+ })));
114
+ }
115
+ }
116
+ catch (err) {
117
+ console.error('Failed to fetch stats:', err);
118
+ }
119
+ finally {
120
+ setStatsLoading(false);
121
+ }
122
+ }, [collectionName]);
123
+ // Fetch table data
124
+ const fetchTableData = (0, react_1.useCallback)(async (tableName) => {
125
+ setLoading(true);
126
+ try {
127
+ const res = await fetch(`/api/vibe/data/${collectionName}/${tableName}?limit=100`);
128
+ if (res.ok) {
129
+ const data = await res.json();
130
+ setTableData(data.data || []);
131
+ }
132
+ }
133
+ catch (err) {
134
+ console.error('Failed to fetch table:', err);
135
+ setTableData([]);
136
+ }
137
+ finally {
138
+ setLoading(false);
139
+ }
140
+ }, [collectionName]);
141
+ // Handle tab change
142
+ const handleTabChange = (0, react_1.useCallback)((tabId) => {
143
+ setActiveTab(tabId);
144
+ const tab = tabs.find(t => t.id === tabId);
145
+ if (tab?.table) {
146
+ fetchTableData(tab.table);
147
+ }
148
+ }, [tabs, fetchTableData]);
149
+ // Initial load
150
+ (0, react_1.useEffect)(() => {
151
+ if (status === 'unauthenticated') {
152
+ router.push(`/account-auth/login?callbackUrl=${encodeURIComponent(window.location.pathname)}`);
153
+ }
154
+ else if (status === 'authenticated' && !hasAccess) {
155
+ router.push('/?error=unauthorized');
156
+ }
157
+ else if (status === 'authenticated' && hasAccess) {
158
+ fetchStats();
159
+ }
160
+ }, [status, hasAccess, router, fetchStats]);
161
+ // Loading state
162
+ if (status === 'loading') {
163
+ return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center ${themeClasses.bg}`, children: (0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-12 w-12 border-b-2 border-amber-500" }) }));
164
+ }
165
+ // Access denied
166
+ if (status === 'authenticated' && !hasAccess) {
167
+ return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center ${themeClasses.bg}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: isDark ? 'text-red-400' : 'text-red-500', children: (0, jsx_runtime_1.jsx)(IconShield, {}) }), (0, jsx_runtime_1.jsx)("h1", { className: `text-2xl font-bold mb-2 ${themeClasses.text}`, children: "Access Denied" }), (0, jsx_runtime_1.jsx)("p", { className: `mb-6 ${themeClasses.textMuted}`, children: "You need admin access for this area." }), (0, jsx_runtime_1.jsxs)(link_1.default, { href: "/", className: "inline-flex items-center gap-2 px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700", children: [(0, jsx_runtime_1.jsx)(IconArrowLeft, {}), " Return to Home"] })] }) }));
168
+ }
169
+ if (status === 'unauthenticated')
170
+ return null;
171
+ const currentTab = tabs.find(t => t.id === activeTab);
172
+ return ((0, jsx_runtime_1.jsxs)("div", { className: `min-h-screen ${themeClasses.bg}`, children: [(0, jsx_runtime_1.jsx)("header", { className: `sticky top-0 z-40 border-b ${themeClasses.headerBg}`, children: (0, jsx_runtime_1.jsx)("div", { className: "container mx-auto px-4 py-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: `p-2 rounded-lg ${isDark ? 'bg-amber-900/30' : 'bg-amber-100'}`, children: (0, jsx_runtime_1.jsx)(IconChart, { className: `w-5 h-5 ${isDark ? 'text-amber-400' : 'text-amber-600'}` }) }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: `text-xl font-bold ${themeClasses.text}`, children: title }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${themeClasses.textMuted}`, children: subtitle })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsxs)("button", { onClick: () => { fetchStats(); if (currentTab?.table)
173
+ fetchTableData(currentTab.table); }, disabled: loading || statsLoading, className: `flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${themeClasses.textMuted} ${isDark ? 'hover:bg-slate-800' : 'hover:bg-gray-100'} ${(loading || statsLoading) ? 'opacity-50' : ''}`, children: [(0, jsx_runtime_1.jsx)("span", { className: (loading || statsLoading) ? 'animate-spin' : '', children: (0, jsx_runtime_1.jsx)(IconRefresh, {}) }), "Refresh"] }), (0, jsx_runtime_1.jsxs)(link_1.default, { href: backUrl, className: `flex items-center gap-2 px-3 py-2 rounded-lg transition-colors ${themeClasses.textMuted} ${isDark ? 'hover:bg-slate-800' : 'hover:bg-gray-100'}`, children: [(0, jsx_runtime_1.jsx)(IconArrowLeft, {}), " ", backLabel] })] })] }) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "container mx-auto px-4 py-6 space-y-6", children: [(0, jsx_runtime_1.jsx)("div", { className: `flex flex-wrap gap-1 p-1 rounded-xl ${themeClasses.tabBg}`, children: tabs.map((tab) => ((0, jsx_runtime_1.jsxs)("button", { onClick: () => handleTabChange(tab.id), className: `py-2 px-4 rounded-lg text-sm font-medium transition-all ${activeTab === tab.id ? themeClasses.tabActive : themeClasses.tabInactive}`, children: [(0, jsx_runtime_1.jsx)("span", { className: "mr-2", children: tab.icon }), tab.label] }, tab.id))) }), renderTabContent ? (renderTabContent(currentTab, tableData, loading)) : activeTab === 'overview' || !currentTab?.table ? (
174
+ // Default Overview
175
+ (0, jsx_runtime_1.jsxs)("div", { className: "space-y-6", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h2", { className: `text-lg font-semibold mb-4 ${themeClasses.text}`, children: "Data Overview" }), (0, jsx_runtime_1.jsx)("div", { className: "grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4", children: stats.map((stat) => ((0, jsx_runtime_1.jsxs)(link_1.default, { href: `/admin/data?collection=${collectionName}&table=${stat.table}`, className: `p-4 rounded-xl border transition-all hover:scale-[1.02] ${themeClasses.cardBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-xs font-medium ${themeClasses.textMuted}`, children: stat.label }), (0, jsx_runtime_1.jsx)("p", { className: `text-2xl font-bold ${themeClasses.text}`, children: statsLoading ? '...' : stat.count.toLocaleString() })] }, stat.table))) })] }), renderOverview && renderOverview(stats)] })) : (
176
+ // Default Table View
177
+ (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("h2", { className: `text-lg font-semibold mb-4 ${themeClasses.text}`, children: [currentTab.label, " (", tableData.length, ")"] }), loading ? ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center py-12", children: (0, jsx_runtime_1.jsx)("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-amber-500" }) })) : ((0, jsx_runtime_1.jsx)("div", { className: `rounded-lg border overflow-hidden ${themeClasses.cardBg}`, children: (0, jsx_runtime_1.jsx)("div", { className: "overflow-x-auto", children: (0, jsx_runtime_1.jsxs)("table", { className: "w-full text-sm", children: [(0, jsx_runtime_1.jsx)("thead", { className: isDark ? 'bg-slate-800' : 'bg-gray-50', children: (0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "ID" }), (0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "User" }), (0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "Data" }), (0, jsx_runtime_1.jsx)("th", { className: `px-4 py-3 text-left font-medium ${themeClasses.textMuted}`, children: "Created" })] }) }), (0, jsx_runtime_1.jsx)("tbody", { className: `divide-y ${isDark ? 'divide-slate-700' : 'divide-gray-200'}`, children: tableData.length === 0 ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { colSpan: 4, className: `px-4 py-8 text-center ${themeClasses.textMuted}`, children: "No data found" }) })) : tableData.map((row) => ((0, jsx_runtime_1.jsxs)("tr", { className: isDark ? 'hover:bg-slate-800/50' : 'hover:bg-gray-50', children: [(0, jsx_runtime_1.jsx)("td", { className: `px-4 py-3 ${themeClasses.text}`, children: row.document_id }), (0, jsx_runtime_1.jsx)("td", { className: `px-4 py-3 ${themeClasses.text}`, children: row.user_id }), (0, jsx_runtime_1.jsxs)("td", { className: `px-4 py-3 ${themeClasses.textMuted} max-w-xs truncate`, children: [JSON.stringify(row.data).substring(0, 50), "..."] }), (0, jsx_runtime_1.jsx)("td", { className: `px-4 py-3 ${themeClasses.textMuted}`, children: new Date(row.created_at).toLocaleDateString() })] }, row.document_id))) })] }) }) }))] }))] })] }));
178
+ }
179
+ exports.default = ClientSiteAdminPage;