@payez/next-mvp 4.1.9 → 4.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +792 -792
- package/dist/api-handlers/auth/refresh.js +38 -15
- package/dist/auth/better-auth.js +4 -3
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/ensure-fresh-access-token.js +42 -8
- package/dist/lib/idp-fetch.js +25 -9
- package/dist/lib/session-store.d.ts +1 -0
- package/dist/lib/session-store.js +12 -8
- package/dist/lib/token-lifecycle.d.ts +2 -0
- package/dist/lib/token-lifecycle.js +21 -3
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/package.json +893 -893
- package/scripts/dev-broker.ps1 +35 -35
- package/scripts/dev-local.ps1 +45 -45
- package/src/api/auth-handler.ts +548 -548
- package/src/api-handlers/account/change-password.ts +142 -142
- package/src/api-handlers/account/update-phone.ts +79 -79
- package/src/api-handlers/account/verify-email.ts +125 -125
- package/src/api-handlers/account/verify-sms.ts +125 -125
- package/src/api-handlers/admin/analytics.ts +443 -443
- package/src/api-handlers/admin/audit.ts +223 -223
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +251 -251
- package/src/api-handlers/admin/sessions.ts +318 -318
- package/src/api-handlers/admin/site-logs.ts +365 -365
- package/src/api-handlers/admin/users.ts +242 -242
- package/src/api-handlers/admin/vibe-data.ts +324 -324
- package/src/api-handlers/auth/refresh.ts +705 -682
- package/src/api-handlers/auth/signout.ts +199 -199
- package/src/api-handlers/auth/status.ts +20 -20
- package/src/api-handlers/auth/update-session.ts +110 -110
- package/src/api-handlers/auth/verify-code.ts +112 -112
- package/src/api-handlers/session/refresh-viability.ts +36 -36
- package/src/api-handlers/test/force-expire.ts +60 -60
- package/src/auth/auth-decision.ts +229 -229
- package/src/auth/better-auth.ts +467 -466
- package/src/auth/route-config.ts +219 -219
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/utils/idp-client.ts +445 -445
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +143 -143
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +266 -266
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +505 -505
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +414 -414
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/FederatedAuthSection.tsx +95 -95
- package/src/components/auth/ModeAwareLoginPage.tsx +135 -135
- package/src/components/auth/ModeAwareSignupPage.tsx +267 -267
- package/src/components/auth/TraditionalAuthSection.tsx +99 -99
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +97 -97
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +57 -57
- package/src/config/logger.ts +62 -62
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +97 -97
- package/src/hooks/useAvailableProviders.ts +120 -120
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/usePublicAuthSettings.ts +149 -149
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +336 -336
- package/src/index.ts +60 -60
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +178 -178
- package/src/lib/ensure-fresh-access-token.ts +369 -335
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-fetch.ts +26 -10
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/roles.ts +177 -177
- package/src/lib/session-store.ts +4 -1
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/token-lifecycle.ts +30 -3
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +663 -663
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/pages/admin-login/page.tsx +221 -221
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +456 -456
- package/src/pages/profile/EnhancedProfilePage.tsx +485 -485
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +234 -234
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +565 -565
- package/src/pages/security/index.ts +9 -9
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +180 -180
- package/src/pages/test-env/JwtInspectPage.tsx +456 -456
- package/src/pages/test-env/RefreshTokenPage.tsx +157 -157
- package/src/pages/test-env/TestEnvPage.tsx +118 -118
- package/src/pages/test-env/index.ts +25 -25
- package/src/pages/verify-code/page.tsx +652 -652
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/auth/logout.ts +108 -108
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/settings.ts +75 -75
- package/src/routes/auth/viability.ts +224 -224
- package/src/server/auth-guard.ts +170 -170
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/with-auth.ts +93 -93
- package/src/stores/authStore.ts +1899 -1899
- package/src/templates/instrumentation.ts +41 -41
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +590 -590
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +185 -185
- package/src/vibe/generic.ts +431 -431
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +132 -132
- package/src/vibe/sessions.ts +551 -551
- package/src/vibe/types.ts +577 -577
|
@@ -1,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
|
+
}
|
package/src/pages/roles/index.ts
CHANGED
|
@@ -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';
|