@payez/next-mvp 4.1.8 → 4.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +792 -792
  2. package/dist/auth/better-auth.js +15 -3
  3. package/dist/components/account/MobileNavDrawer.js +18 -18
  4. package/dist/components/account/UserAvatarMenu.js +1 -1
  5. package/dist/components/admin/AlertSettingsTab.js +40 -40
  6. package/dist/components/admin/LoggingSettingsTab.js +28 -28
  7. package/dist/lib/idp-fetch.js +9 -25
  8. package/dist/pages/login/page.js +28 -28
  9. package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
  10. package/package.json +1 -1
  11. package/scripts/dev-broker.ps1 +35 -35
  12. package/scripts/dev-local.ps1 +45 -45
  13. package/src/api/auth-handler.ts +548 -548
  14. package/src/api-handlers/account/change-password.ts +142 -142
  15. package/src/api-handlers/account/update-phone.ts +79 -79
  16. package/src/api-handlers/account/verify-email.ts +125 -125
  17. package/src/api-handlers/account/verify-sms.ts +125 -125
  18. package/src/api-handlers/admin/analytics.ts +443 -443
  19. package/src/api-handlers/admin/audit.ts +223 -223
  20. package/src/api-handlers/admin/index.ts +63 -63
  21. package/src/api-handlers/admin/redis-sessions.ts +251 -251
  22. package/src/api-handlers/admin/sessions.ts +318 -318
  23. package/src/api-handlers/admin/site-logs.ts +365 -365
  24. package/src/api-handlers/admin/users.ts +242 -242
  25. package/src/api-handlers/admin/vibe-data.ts +324 -324
  26. package/src/api-handlers/auth/refresh.ts +682 -682
  27. package/src/api-handlers/auth/signout.ts +199 -199
  28. package/src/api-handlers/auth/status.ts +20 -20
  29. package/src/api-handlers/auth/update-session.ts +110 -110
  30. package/src/api-handlers/auth/verify-code.ts +112 -112
  31. package/src/api-handlers/session/refresh-viability.ts +36 -36
  32. package/src/api-handlers/test/force-expire.ts +60 -60
  33. package/src/auth/auth-decision.ts +229 -229
  34. package/src/auth/better-auth.ts +15 -3
  35. package/src/auth/route-config.ts +219 -219
  36. package/src/auth/types/auth-types.ts +555 -555
  37. package/src/auth/types/index.ts +7 -7
  38. package/src/auth/utils/idp-client.ts +445 -445
  39. package/src/auth/utils/index.ts +6 -6
  40. package/src/auth/utils/token-utils.ts +243 -243
  41. package/src/client/AuthContext.tsx +143 -143
  42. package/src/client/fetch-with-auth.ts +47 -47
  43. package/src/components/SessionSync.tsx +138 -138
  44. package/src/components/SignalRHealthCheck.tsx +131 -131
  45. package/src/components/account/MobileNavDrawer.tsx +305 -305
  46. package/src/components/account/UserAvatarMenu.tsx +266 -266
  47. package/src/components/account/index.ts +13 -13
  48. package/src/components/admin/AlertSettingsTab.tsx +728 -728
  49. package/src/components/admin/AnalyticsTab.tsx +703 -703
  50. package/src/components/admin/DataBrowserTab.tsx +505 -505
  51. package/src/components/admin/LoggingSettingsTab.tsx +665 -665
  52. package/src/components/admin/SessionsTab.tsx +414 -414
  53. package/src/components/admin/StatsTab.tsx +379 -379
  54. package/src/components/admin/VibeAdminLayout.tsx +187 -187
  55. package/src/components/admin/index.ts +59 -59
  56. package/src/components/auth/FederatedAuthSection.tsx +95 -95
  57. package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
  58. package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
  59. package/src/components/auth/TraditionalAuthSection.tsx +99 -99
  60. package/src/components/recovery/CompleteStep.tsx +36 -36
  61. package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
  62. package/src/components/recovery/SelectMethodStep.tsx +73 -73
  63. package/src/components/recovery/SetPasswordStep.tsx +97 -97
  64. package/src/components/recovery/VerifyCodeStep.tsx +90 -90
  65. package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
  66. package/src/components/reserved/ReservedStatusBox.tsx +118 -118
  67. package/src/components/ui/BetaBadge.tsx +58 -58
  68. package/src/components/ui/Footer.tsx +93 -93
  69. package/src/config/env.ts +57 -57
  70. package/src/config/logger.ts +62 -62
  71. package/src/config/vibe-log-transport.ts +264 -264
  72. package/src/hooks/useAuth.ts +117 -117
  73. package/src/hooks/useAuthSettings.ts +97 -97
  74. package/src/hooks/useAvailableProviders.ts +120 -120
  75. package/src/hooks/usePasswordValidation.ts +127 -127
  76. package/src/hooks/usePublicAuthSettings.ts +149 -149
  77. package/src/hooks/useSessionExpiration.ts +101 -101
  78. package/src/hooks/useViabilitySession.ts +336 -336
  79. package/src/index.ts +60 -60
  80. package/src/lib/api-handler.ts +643 -643
  81. package/src/lib/app-slug.ts +178 -178
  82. package/src/lib/geolocation.ts +265 -265
  83. package/src/lib/idp-fetch.ts +10 -26
  84. package/src/lib/jwt-decode.ts +82 -82
  85. package/src/lib/roles.ts +177 -177
  86. package/src/lib/site-logger.ts +245 -245
  87. package/src/lib/standardized-client-api.ts +901 -901
  88. package/src/lib/test-aware-get-token.ts +41 -41
  89. package/src/lib/user-agent-parser.ts +252 -252
  90. package/src/logging/api/admin-analytics.ts +51 -51
  91. package/src/logging/api/audit-log.ts +53 -53
  92. package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
  93. package/src/logging/components/AuditLogViewer.tsx +125 -125
  94. package/src/logging/components/ErrorMetricsCard.tsx +98 -98
  95. package/src/logging/components/HealthMetricsCard.tsx +70 -70
  96. package/src/logging/hooks/useAdminAnalytics.ts +22 -22
  97. package/src/logging/hooks/useAuditLog.ts +24 -24
  98. package/src/logging/hooks/useErrorMetrics.ts +40 -40
  99. package/src/logging/hooks/useHealthMetrics.ts +44 -44
  100. package/src/logging/index.ts +18 -18
  101. package/src/logging/types/analytics.ts +81 -81
  102. package/src/logging/types/audit.ts +31 -31
  103. package/src/logging/types/index.ts +3 -3
  104. package/src/middleware/create-middleware.ts +663 -663
  105. package/src/middleware/rbac-check.ts +279 -279
  106. package/src/middleware/twofa-presets.ts +224 -224
  107. package/src/pages/admin-login/page.tsx +221 -221
  108. package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
  109. package/src/pages/admin-page-permissions/index.ts +7 -7
  110. package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
  111. package/src/pages/admin-roles/modals.tsx +469 -469
  112. package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
  113. package/src/pages/client-admin/index.ts +33 -33
  114. package/src/pages/coming-soon/page.tsx +83 -83
  115. package/src/pages/login/page.tsx +456 -456
  116. package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
  117. package/src/pages/profile/index.ts +9 -9
  118. package/src/pages/profile/page.tsx +166 -166
  119. package/src/pages/recovery/page.tsx +234 -234
  120. package/src/pages/roles/MyRolesPage.tsx +211 -211
  121. package/src/pages/roles/components.tsx +294 -294
  122. package/src/pages/roles/index.ts +17 -17
  123. package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
  124. package/src/pages/security/index.ts +9 -9
  125. package/src/pages/security/page.tsx +507 -507
  126. package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
  127. package/src/pages/settings/index.ts +9 -9
  128. package/src/pages/settings/page.tsx +47 -47
  129. package/src/pages/showcase/ShowcasePage.tsx +532 -532
  130. package/src/pages/showcase/index.ts +13 -13
  131. package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
  132. package/src/pages/test-env/JwtInspectPage.tsx +456 -456
  133. package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
  134. package/src/pages/test-env/TestEnvPage.tsx +118 -118
  135. package/src/pages/test-env/index.ts +25 -25
  136. package/src/pages/verify-code/page.tsx +652 -652
  137. package/src/routes/account/update-phone.ts +13 -13
  138. package/src/routes/auth/logout.ts +108 -108
  139. package/src/routes/auth/nextauth.ts +45 -45
  140. package/src/routes/auth/settings.ts +75 -75
  141. package/src/routes/auth/viability.ts +224 -224
  142. package/src/server/auth-guard.ts +170 -170
  143. package/src/server/slim-middleware.ts +117 -117
  144. package/src/server/with-auth.ts +93 -93
  145. package/src/stores/authStore.ts +1899 -1899
  146. package/src/templates/instrumentation.ts +41 -41
  147. package/src/utils/logout.ts +30 -30
  148. package/src/vibe/client.ts +590 -590
  149. package/src/vibe/enterprise-auth.ts +208 -208
  150. package/src/vibe/errors.ts +185 -185
  151. package/src/vibe/generic.ts +431 -431
  152. package/src/vibe/hooks/index.ts +367 -367
  153. package/src/vibe/index.ts +132 -132
  154. package/src/vibe/sessions.ts +551 -551
  155. package/src/vibe/types.ts +577 -577
@@ -1,294 +1,294 @@
1
- /**
2
- * Shared Role Components for @payez/next-mvp
3
- *
4
- * Components for displaying roles and permissions:
5
- * - RoleBadge: Small badge showing role name and source
6
- * - RoleCard: Expandable card showing role details and permissions
7
- * - PermissionsList: Grouped list of permissions
8
- *
9
- * @see docs/specs/ROLES_MANAGEMENT_SPEC.md
10
- */
11
-
12
- 'use client';
13
-
14
- import React, { useState } from 'react';
15
-
16
- // Types
17
- export interface Permission {
18
- type: 'page' | 'feature';
19
- pattern?: string;
20
- name?: string;
21
- display: string;
22
- }
23
-
24
- export interface Role {
25
- role_name: string;
26
- display_name?: string;
27
- description?: string;
28
- assigned_at?: string;
29
- status?: 'active' | 'pending' | 'expired';
30
- permissions?: Permission[];
31
- user_count?: number;
32
- permission_count?: number;
33
- is_system_role?: boolean;
34
- }
35
-
36
- // Role source type
37
- export type RoleSource = 'idp' | 'app';
38
-
39
- // Format date helper
40
- function formatDate(dateString?: string): string {
41
- if (!dateString) return '';
42
- try {
43
- return new Date(dateString).toLocaleDateString('en-US', {
44
- year: 'numeric',
45
- month: 'long',
46
- day: 'numeric',
47
- });
48
- } catch {
49
- return dateString;
50
- }
51
- }
52
-
53
- // Role icon based on name
54
- function getRoleIcon(roleName: string): string {
55
- if (roleName.includes('admin')) return 'shield';
56
- if (roleName.includes('editor')) return 'edit';
57
- if (roleName.includes('viewer')) return 'eye';
58
- return 'user';
59
- }
60
-
61
- /**
62
- * RoleBadge - Small badge showing role name and source
63
- */
64
- export function RoleBadge({
65
- roleName,
66
- source,
67
- size = 'md',
68
- isDark = true,
69
- }: {
70
- roleName: string;
71
- source: RoleSource;
72
- size?: 'sm' | 'md';
73
- isDark?: boolean;
74
- }) {
75
- const sizeClasses = size === 'sm' ? 'text-xs px-2 py-0.5' : 'text-sm px-2.5 py-1';
76
- const sourceColor = source === 'idp'
77
- ? isDark ? 'bg-purple-900/30 text-purple-400 border-purple-500/30' : 'bg-purple-100 text-purple-700 border-purple-300'
78
- : isDark ? 'bg-blue-900/30 text-blue-400 border-blue-500/30' : 'bg-blue-100 text-blue-700 border-blue-300';
79
-
80
- return (
81
- <span className={`inline-flex items-center gap-1 rounded-full border ${sizeClasses} ${sourceColor}`}>
82
- {source === 'idp' && (
83
- <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
84
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
85
- </svg>
86
- )}
87
- {roleName}
88
- </span>
89
- );
90
- }
91
-
92
- /**
93
- * PermissionsList - Grouped list of permissions
94
- */
95
- export function PermissionsList({
96
- permissions,
97
- grouped = true,
98
- isDark = true,
99
- }: {
100
- permissions: Permission[];
101
- grouped?: boolean;
102
- isDark?: boolean;
103
- }) {
104
- const textPrimary = isDark ? 'text-white' : 'text-gray-900';
105
- const textMuted = isDark ? 'text-slate-400' : 'text-gray-500';
106
- const borderColor = isDark ? 'border-slate-600' : 'border-gray-300';
107
-
108
- if (!permissions || permissions.length === 0) {
109
- return <p className={`text-sm ${textMuted}`}>No specific permissions defined</p>;
110
- }
111
-
112
- if (!grouped) {
113
- return (
114
- <ul className="space-y-1">
115
- {permissions.map((perm, idx) => (
116
- <li key={idx} className={`text-sm ${textMuted} flex items-center gap-2`}>
117
- <span className="text-green-400">+</span>
118
- {perm.display}
119
- </li>
120
- ))}
121
- </ul>
122
- );
123
- }
124
-
125
- // Group by type
126
- const pagePerms = permissions.filter((p) => p.type === 'page');
127
- const featurePerms = permissions.filter((p) => p.type === 'feature');
128
-
129
- return (
130
- <div className="space-y-3">
131
- {pagePerms.length > 0 && (
132
- <div>
133
- <p className={`text-xs font-medium uppercase tracking-wider mb-1 ${textMuted}`}>Page Access</p>
134
- <ul className={`pl-4 border-l-2 ${borderColor} space-y-1`}>
135
- {pagePerms.map((perm, idx) => (
136
- <li key={idx} className={`text-sm ${textPrimary}`}>{perm.display}</li>
137
- ))}
138
- </ul>
139
- </div>
140
- )}
141
- {featurePerms.length > 0 && (
142
- <div>
143
- <p className={`text-xs font-medium uppercase tracking-wider mb-1 ${textMuted}`}>Features</p>
144
- <ul className={`pl-4 border-l-2 ${borderColor} space-y-1`}>
145
- {featurePerms.map((perm, idx) => (
146
- <li key={idx} className={`text-sm ${textPrimary}`}>{perm.display}</li>
147
- ))}
148
- </ul>
149
- </div>
150
- )}
151
- </div>
152
- );
153
- }
154
-
155
- /**
156
- * RoleCard - Expandable card showing role details and permissions
157
- */
158
- export function RoleCard({
159
- role,
160
- source,
161
- showPermissions = true,
162
- defaultExpanded = false,
163
- isDark = true,
164
- }: {
165
- role: Role;
166
- source: RoleSource;
167
- showPermissions?: boolean;
168
- defaultExpanded?: boolean;
169
- isDark?: boolean;
170
- }) {
171
- const [expanded, setExpanded] = useState(defaultExpanded);
172
-
173
- const cardBg = isDark ? 'bg-slate-800' : 'bg-white';
174
- const borderColor = isDark ? 'border-slate-700' : 'border-gray-200';
175
- const textPrimary = isDark ? 'text-white' : 'text-gray-900';
176
- const textMuted = isDark ? 'text-slate-400' : 'text-gray-500';
177
- const hoverBg = isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50';
178
-
179
- const statusColors = {
180
- active: isDark ? 'text-green-400' : 'text-green-600',
181
- pending: isDark ? 'text-yellow-400' : 'text-yellow-600',
182
- expired: isDark ? 'text-red-400' : 'text-red-600',
183
- };
184
-
185
- const iconMap: Record<string, React.ReactNode> = {
186
- shield: (
187
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
188
- <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" />
189
- </svg>
190
- ),
191
- edit: (
192
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
193
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
194
- </svg>
195
- ),
196
- eye: (
197
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
198
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
199
- <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" />
200
- </svg>
201
- ),
202
- user: (
203
- <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
204
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
205
- </svg>
206
- ),
207
- };
208
-
209
- const icon = iconMap[getRoleIcon(role.role_name)] || iconMap.user;
210
-
211
- return (
212
- <div className={`rounded-lg border ${borderColor} ${cardBg} overflow-hidden`}>
213
- {/* Header */}
214
- <div className={`p-4 ${showPermissions ? `cursor-pointer ${hoverBg}` : ''}`} onClick={() => showPermissions && setExpanded(!expanded)}>
215
- <div className="flex items-start justify-between">
216
- <div className="flex items-center gap-3">
217
- <div className={`p-2 rounded-lg ${isDark ? 'bg-slate-700' : 'bg-gray-100'} ${textMuted}`}>
218
- {icon}
219
- </div>
220
- <div>
221
- <div className="flex items-center gap-2">
222
- <h3 className={`font-semibold ${textPrimary}`}>{role.display_name || role.role_name}</h3>
223
- {source === 'idp' && (
224
- <svg className={`w-4 h-4 ${textMuted}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
225
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
226
- </svg>
227
- )}
228
- </div>
229
- <p className={`text-sm ${textMuted}`}>{role.description || 'No description'}</p>
230
- {role.assigned_at && (
231
- <p className={`text-xs ${textMuted} mt-1`}>Assigned: {formatDate(role.assigned_at)}</p>
232
- )}
233
- </div>
234
- </div>
235
- <div className="flex items-center gap-2">
236
- {role.status && (
237
- <span className={`flex items-center gap-1 text-sm ${statusColors[role.status]}`}>
238
- <span className="w-2 h-2 rounded-full bg-current" />
239
- {role.status.charAt(0).toUpperCase() + role.status.slice(1)}
240
- </span>
241
- )}
242
- {showPermissions && (
243
- <svg
244
- className={`w-5 h-5 transition-transform ${expanded ? 'rotate-180' : ''} ${textMuted}`}
245
- fill="none"
246
- viewBox="0 0 24 24"
247
- stroke="currentColor"
248
- >
249
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
250
- </svg>
251
- )}
252
- </div>
253
- </div>
254
- </div>
255
-
256
- {/* Permissions (expandable) */}
257
- {showPermissions && expanded && role.permissions && (
258
- <div className={`px-4 pb-4 pt-2 border-t ${borderColor}`}>
259
- <PermissionsList permissions={role.permissions} isDark={isDark} />
260
- </div>
261
- )}
262
- </div>
263
- );
264
- }
265
-
266
- /**
267
- * RoleSourceHeader - Section header for role groups
268
- */
269
- export function RoleSourceHeader({
270
- source,
271
- count,
272
- isDark = true,
273
- }: {
274
- source: RoleSource;
275
- count?: number;
276
- isDark?: boolean;
277
- }) {
278
- const bgColor = isDark ? 'bg-slate-700' : 'bg-gray-100';
279
- const textColor = isDark ? 'text-slate-300' : 'text-gray-700';
280
-
281
- const labels = {
282
- idp: 'Identity Roles (from PayEz IDP)',
283
- app: 'Application Roles',
284
- };
285
-
286
- return (
287
- <div className={`${bgColor} px-4 py-2 rounded-lg flex items-center justify-between`}>
288
- <span className={`text-sm font-medium ${textColor}`}>{labels[source]}</span>
289
- {count !== undefined && (
290
- <span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{count} role{count !== 1 ? 's' : ''}</span>
291
- )}
292
- </div>
293
- );
294
- }
1
+ /**
2
+ * Shared Role Components for @payez/next-mvp
3
+ *
4
+ * Components for displaying roles and permissions:
5
+ * - RoleBadge: Small badge showing role name and source
6
+ * - RoleCard: Expandable card showing role details and permissions
7
+ * - PermissionsList: Grouped list of permissions
8
+ *
9
+ * @see docs/specs/ROLES_MANAGEMENT_SPEC.md
10
+ */
11
+
12
+ 'use client';
13
+
14
+ import React, { useState } from 'react';
15
+
16
+ // Types
17
+ export interface Permission {
18
+ type: 'page' | 'feature';
19
+ pattern?: string;
20
+ name?: string;
21
+ display: string;
22
+ }
23
+
24
+ export interface Role {
25
+ role_name: string;
26
+ display_name?: string;
27
+ description?: string;
28
+ assigned_at?: string;
29
+ status?: 'active' | 'pending' | 'expired';
30
+ permissions?: Permission[];
31
+ user_count?: number;
32
+ permission_count?: number;
33
+ is_system_role?: boolean;
34
+ }
35
+
36
+ // Role source type
37
+ export type RoleSource = 'idp' | 'app';
38
+
39
+ // Format date helper
40
+ function formatDate(dateString?: string): string {
41
+ if (!dateString) return '';
42
+ try {
43
+ return new Date(dateString).toLocaleDateString('en-US', {
44
+ year: 'numeric',
45
+ month: 'long',
46
+ day: 'numeric',
47
+ });
48
+ } catch {
49
+ return dateString;
50
+ }
51
+ }
52
+
53
+ // Role icon based on name
54
+ function getRoleIcon(roleName: string): string {
55
+ if (roleName.includes('admin')) return 'shield';
56
+ if (roleName.includes('editor')) return 'edit';
57
+ if (roleName.includes('viewer')) return 'eye';
58
+ return 'user';
59
+ }
60
+
61
+ /**
62
+ * RoleBadge - Small badge showing role name and source
63
+ */
64
+ export function RoleBadge({
65
+ roleName,
66
+ source,
67
+ size = 'md',
68
+ isDark = true,
69
+ }: {
70
+ roleName: string;
71
+ source: RoleSource;
72
+ size?: 'sm' | 'md';
73
+ isDark?: boolean;
74
+ }) {
75
+ const sizeClasses = size === 'sm' ? 'text-xs px-2 py-0.5' : 'text-sm px-2.5 py-1';
76
+ const sourceColor = source === 'idp'
77
+ ? isDark ? 'bg-purple-900/30 text-purple-400 border-purple-500/30' : 'bg-purple-100 text-purple-700 border-purple-300'
78
+ : isDark ? 'bg-blue-900/30 text-blue-400 border-blue-500/30' : 'bg-blue-100 text-blue-700 border-blue-300';
79
+
80
+ return (
81
+ <span className={`inline-flex items-center gap-1 rounded-full border ${sizeClasses} ${sourceColor}`}>
82
+ {source === 'idp' && (
83
+ <svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
84
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
85
+ </svg>
86
+ )}
87
+ {roleName}
88
+ </span>
89
+ );
90
+ }
91
+
92
+ /**
93
+ * PermissionsList - Grouped list of permissions
94
+ */
95
+ export function PermissionsList({
96
+ permissions,
97
+ grouped = true,
98
+ isDark = true,
99
+ }: {
100
+ permissions: Permission[];
101
+ grouped?: boolean;
102
+ isDark?: boolean;
103
+ }) {
104
+ const textPrimary = isDark ? 'text-white' : 'text-gray-900';
105
+ const textMuted = isDark ? 'text-slate-400' : 'text-gray-500';
106
+ const borderColor = isDark ? 'border-slate-600' : 'border-gray-300';
107
+
108
+ if (!permissions || permissions.length === 0) {
109
+ return <p className={`text-sm ${textMuted}`}>No specific permissions defined</p>;
110
+ }
111
+
112
+ if (!grouped) {
113
+ return (
114
+ <ul className="space-y-1">
115
+ {permissions.map((perm, idx) => (
116
+ <li key={idx} className={`text-sm ${textMuted} flex items-center gap-2`}>
117
+ <span className="text-green-400">+</span>
118
+ {perm.display}
119
+ </li>
120
+ ))}
121
+ </ul>
122
+ );
123
+ }
124
+
125
+ // Group by type
126
+ const pagePerms = permissions.filter((p) => p.type === 'page');
127
+ const featurePerms = permissions.filter((p) => p.type === 'feature');
128
+
129
+ return (
130
+ <div className="space-y-3">
131
+ {pagePerms.length > 0 && (
132
+ <div>
133
+ <p className={`text-xs font-medium uppercase tracking-wider mb-1 ${textMuted}`}>Page Access</p>
134
+ <ul className={`pl-4 border-l-2 ${borderColor} space-y-1`}>
135
+ {pagePerms.map((perm, idx) => (
136
+ <li key={idx} className={`text-sm ${textPrimary}`}>{perm.display}</li>
137
+ ))}
138
+ </ul>
139
+ </div>
140
+ )}
141
+ {featurePerms.length > 0 && (
142
+ <div>
143
+ <p className={`text-xs font-medium uppercase tracking-wider mb-1 ${textMuted}`}>Features</p>
144
+ <ul className={`pl-4 border-l-2 ${borderColor} space-y-1`}>
145
+ {featurePerms.map((perm, idx) => (
146
+ <li key={idx} className={`text-sm ${textPrimary}`}>{perm.display}</li>
147
+ ))}
148
+ </ul>
149
+ </div>
150
+ )}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ /**
156
+ * RoleCard - Expandable card showing role details and permissions
157
+ */
158
+ export function RoleCard({
159
+ role,
160
+ source,
161
+ showPermissions = true,
162
+ defaultExpanded = false,
163
+ isDark = true,
164
+ }: {
165
+ role: Role;
166
+ source: RoleSource;
167
+ showPermissions?: boolean;
168
+ defaultExpanded?: boolean;
169
+ isDark?: boolean;
170
+ }) {
171
+ const [expanded, setExpanded] = useState(defaultExpanded);
172
+
173
+ const cardBg = isDark ? 'bg-slate-800' : 'bg-white';
174
+ const borderColor = isDark ? 'border-slate-700' : 'border-gray-200';
175
+ const textPrimary = isDark ? 'text-white' : 'text-gray-900';
176
+ const textMuted = isDark ? 'text-slate-400' : 'text-gray-500';
177
+ const hoverBg = isDark ? 'hover:bg-slate-700/50' : 'hover:bg-gray-50';
178
+
179
+ const statusColors = {
180
+ active: isDark ? 'text-green-400' : 'text-green-600',
181
+ pending: isDark ? 'text-yellow-400' : 'text-yellow-600',
182
+ expired: isDark ? 'text-red-400' : 'text-red-600',
183
+ };
184
+
185
+ const iconMap: Record<string, React.ReactNode> = {
186
+ shield: (
187
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
188
+ <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" />
189
+ </svg>
190
+ ),
191
+ edit: (
192
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
193
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
194
+ </svg>
195
+ ),
196
+ eye: (
197
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
198
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
199
+ <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" />
200
+ </svg>
201
+ ),
202
+ user: (
203
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
204
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
205
+ </svg>
206
+ ),
207
+ };
208
+
209
+ const icon = iconMap[getRoleIcon(role.role_name)] || iconMap.user;
210
+
211
+ return (
212
+ <div className={`rounded-lg border ${borderColor} ${cardBg} overflow-hidden`}>
213
+ {/* Header */}
214
+ <div className={`p-4 ${showPermissions ? `cursor-pointer ${hoverBg}` : ''}`} onClick={() => showPermissions && setExpanded(!expanded)}>
215
+ <div className="flex items-start justify-between">
216
+ <div className="flex items-center gap-3">
217
+ <div className={`p-2 rounded-lg ${isDark ? 'bg-slate-700' : 'bg-gray-100'} ${textMuted}`}>
218
+ {icon}
219
+ </div>
220
+ <div>
221
+ <div className="flex items-center gap-2">
222
+ <h3 className={`font-semibold ${textPrimary}`}>{role.display_name || role.role_name}</h3>
223
+ {source === 'idp' && (
224
+ <svg className={`w-4 h-4 ${textMuted}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
225
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
226
+ </svg>
227
+ )}
228
+ </div>
229
+ <p className={`text-sm ${textMuted}`}>{role.description || 'No description'}</p>
230
+ {role.assigned_at && (
231
+ <p className={`text-xs ${textMuted} mt-1`}>Assigned: {formatDate(role.assigned_at)}</p>
232
+ )}
233
+ </div>
234
+ </div>
235
+ <div className="flex items-center gap-2">
236
+ {role.status && (
237
+ <span className={`flex items-center gap-1 text-sm ${statusColors[role.status]}`}>
238
+ <span className="w-2 h-2 rounded-full bg-current" />
239
+ {role.status.charAt(0).toUpperCase() + role.status.slice(1)}
240
+ </span>
241
+ )}
242
+ {showPermissions && (
243
+ <svg
244
+ className={`w-5 h-5 transition-transform ${expanded ? 'rotate-180' : ''} ${textMuted}`}
245
+ fill="none"
246
+ viewBox="0 0 24 24"
247
+ stroke="currentColor"
248
+ >
249
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
250
+ </svg>
251
+ )}
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ {/* Permissions (expandable) */}
257
+ {showPermissions && expanded && role.permissions && (
258
+ <div className={`px-4 pb-4 pt-2 border-t ${borderColor}`}>
259
+ <PermissionsList permissions={role.permissions} isDark={isDark} />
260
+ </div>
261
+ )}
262
+ </div>
263
+ );
264
+ }
265
+
266
+ /**
267
+ * RoleSourceHeader - Section header for role groups
268
+ */
269
+ export function RoleSourceHeader({
270
+ source,
271
+ count,
272
+ isDark = true,
273
+ }: {
274
+ source: RoleSource;
275
+ count?: number;
276
+ isDark?: boolean;
277
+ }) {
278
+ const bgColor = isDark ? 'bg-slate-700' : 'bg-gray-100';
279
+ const textColor = isDark ? 'text-slate-300' : 'text-gray-700';
280
+
281
+ const labels = {
282
+ idp: 'Identity Roles (from PayEz IDP)',
283
+ app: 'Application Roles',
284
+ };
285
+
286
+ return (
287
+ <div className={`${bgColor} px-4 py-2 rounded-lg flex items-center justify-between`}>
288
+ <span className={`text-sm font-medium ${textColor}`}>{labels[source]}</span>
289
+ {count !== undefined && (
290
+ <span className={`text-xs ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{count} role{count !== 1 ? 's' : ''}</span>
291
+ )}
292
+ </div>
293
+ );
294
+ }
@@ -1,17 +1,17 @@
1
- /**
2
- * Roles Page exports
3
- *
4
- * - MyRolesPage: User view of their roles (/account/roles)
5
- * - Components: RoleCard, RoleBadge, PermissionsList, RoleSourceHeader
6
- */
7
-
8
- export { default as MyRolesPage } from './MyRolesPage';
9
- export {
10
- RoleCard,
11
- RoleBadge,
12
- PermissionsList,
13
- RoleSourceHeader,
14
- type Role,
15
- type Permission,
16
- type RoleSource,
17
- } from './components';
1
+ /**
2
+ * Roles Page exports
3
+ *
4
+ * - MyRolesPage: User view of their roles (/account/roles)
5
+ * - Components: RoleCard, RoleBadge, PermissionsList, RoleSourceHeader
6
+ */
7
+
8
+ export { default as MyRolesPage } from './MyRolesPage';
9
+ export {
10
+ RoleCard,
11
+ RoleBadge,
12
+ PermissionsList,
13
+ RoleSourceHeader,
14
+ type Role,
15
+ type Permission,
16
+ type RoleSource,
17
+ } from './components';