hazo_auth 3.0.4 → 4.1.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.
- package/README.md +228 -8
- package/SETUP_CHECKLIST.md +370 -0
- package/dist/app/api/hazo_auth/me/route.d.ts +3 -0
- package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
- package/dist/app/api/hazo_auth/me/route.js +9 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.d.ts.map +1 -1
- package/dist/components/layouts/my_settings/components/profile_picture_library_tab.js +2 -2
- package/dist/components/layouts/profile_stamp_test/index.d.ts +10 -0
- package/dist/components/layouts/profile_stamp_test/index.d.ts.map +1 -0
- package/dist/components/layouts/profile_stamp_test/index.js +51 -0
- package/dist/components/layouts/rbac_test/index.d.ts +15 -0
- package/dist/components/layouts/rbac_test/index.d.ts.map +1 -0
- package/dist/components/layouts/rbac_test/index.js +378 -0
- package/dist/components/layouts/shared/components/password_field.js +1 -1
- package/dist/components/layouts/shared/components/profile_stamp.d.ts +58 -0
- package/dist/components/layouts/shared/components/profile_stamp.d.ts.map +1 -0
- package/dist/components/layouts/shared/components/profile_stamp.js +72 -0
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
- package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +2 -2
- package/dist/components/layouts/shared/components/two_column_auth_layout.js +1 -1
- package/dist/components/layouts/shared/hooks/use_auth_status.d.ts +3 -0
- package/dist/components/layouts/shared/hooks/use_auth_status.d.ts.map +1 -1
- package/dist/components/layouts/shared/hooks/use_auth_status.js +4 -0
- package/dist/components/layouts/shared/index.d.ts +2 -0
- package/dist/components/layouts/shared/index.d.ts.map +1 -1
- package/dist/components/layouts/shared/index.js +1 -0
- package/dist/components/layouts/user_management/components/roles_matrix.d.ts +2 -3
- package/dist/components/layouts/user_management/components/roles_matrix.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/roles_matrix.js +133 -8
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts +12 -0
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/scope_hierarchy_tab.js +291 -0
- package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts +13 -0
- package/dist/components/layouts/user_management/components/scope_labels_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/scope_labels_tab.js +158 -0
- package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts +11 -0
- package/dist/components/layouts/user_management/components/user_scopes_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/user_scopes_tab.js +267 -0
- package/dist/components/layouts/user_management/index.d.ts +9 -2
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +22 -6
- package/dist/components/ui/hover-card.d.ts +7 -0
- package/dist/components/ui/hover-card.d.ts.map +1 -0
- package/dist/components/ui/hover-card.js +29 -0
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +1 -0
- package/dist/components/ui/select.d.ts +14 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/select.js +59 -0
- package/dist/components/ui/tree-view.d.ts +108 -0
- package/dist/components/ui/tree-view.d.ts.map +1 -0
- package/dist/components/ui/tree-view.js +194 -0
- package/dist/lib/auth/auth_types.d.ts +45 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/auth_types.js +13 -0
- package/dist/lib/auth/hazo_get_auth.server.d.ts +4 -2
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +107 -3
- package/dist/lib/auth/scope_cache.d.ts +92 -0
- package/dist/lib/auth/scope_cache.d.ts.map +1 -0
- package/dist/lib/auth/scope_cache.js +171 -0
- package/dist/lib/scope_hierarchy_config.server.d.ts +39 -0
- package/dist/lib/scope_hierarchy_config.server.d.ts.map +1 -0
- package/dist/lib/scope_hierarchy_config.server.js +96 -0
- package/dist/lib/services/email_service.d.ts.map +1 -1
- package/dist/lib/services/email_service.js +7 -2
- package/dist/lib/services/profile_picture_service.d.ts +1 -7
- package/dist/lib/services/profile_picture_service.d.ts.map +1 -1
- package/dist/lib/services/profile_picture_service.js +77 -32
- package/dist/lib/services/registration_service.js +1 -1
- package/dist/lib/services/scope_labels_service.d.ts +48 -0
- package/dist/lib/services/scope_labels_service.d.ts.map +1 -0
- package/dist/lib/services/scope_labels_service.js +277 -0
- package/dist/lib/services/scope_service.d.ts +114 -0
- package/dist/lib/services/scope_service.d.ts.map +1 -0
- package/dist/lib/services/scope_service.js +582 -0
- package/dist/lib/services/user_scope_service.d.ts +74 -0
- package/dist/lib/services/user_scope_service.d.ts.map +1 -0
- package/dist/lib/services/user_scope_service.js +415 -0
- package/hazo_auth_config.example.ini +1 -1
- package/package.json +4 -1
|
@@ -24,6 +24,9 @@ import { get_filename, get_line_number } from "../../../../lib/utils/api_route_h
|
|
|
24
24
|
* email_verified: boolean,
|
|
25
25
|
* last_logon: string | undefined,
|
|
26
26
|
* profile_picture_url: string | null,
|
|
27
|
+
* profile_image: string | null, // alias for profile_picture_url
|
|
28
|
+
* avatar_url: string | null, // alias for profile_picture_url
|
|
29
|
+
* image: string | null, // alias for profile_picture_url
|
|
27
30
|
* profile_source: "upload" | "library" | "gravatar" | "custom" | undefined,
|
|
28
31
|
* user: { id, email_address, name, is_active, profile_picture_url },
|
|
29
32
|
* permissions: string[],
|
|
@@ -63,6 +66,7 @@ export async function GET(request) {
|
|
|
63
66
|
const profile_source_db = user_db.profile_source;
|
|
64
67
|
const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
|
|
65
68
|
// Return unified format with all fields
|
|
69
|
+
const profile_pic = auth_result.user.profile_picture_url;
|
|
66
70
|
return NextResponse.json({
|
|
67
71
|
authenticated: true,
|
|
68
72
|
// Top-level fields for backward compatibility
|
|
@@ -71,7 +75,11 @@ export async function GET(request) {
|
|
|
71
75
|
name: auth_result.user.name,
|
|
72
76
|
email_verified: user_db.email_verified === true,
|
|
73
77
|
last_logon: user_db.last_logon || undefined,
|
|
74
|
-
profile_picture_url:
|
|
78
|
+
profile_picture_url: profile_pic,
|
|
79
|
+
// Aliases for profile_picture_url (for consuming app compatibility)
|
|
80
|
+
profile_image: profile_pic,
|
|
81
|
+
avatar_url: profile_pic,
|
|
82
|
+
image: profile_pic,
|
|
75
83
|
profile_source: profile_source_ui,
|
|
76
84
|
// Permissions and user object (always included)
|
|
77
85
|
user: auth_result.user,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile_picture_library_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_library_tab.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAGF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,QAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,EAAE,6BAA6B,
|
|
1
|
+
{"version":3,"file":"profile_picture_library_tab.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/my_settings/components/profile_picture_library_tab.tsx"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,uBAAuB,EAAE,MAAM,CAAC;IAChC,uBAAuB,EAAE,MAAM,CAAC;CACjC,CAAC;AAGF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,QAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,uBAAuB,EACvB,uBAAuB,GACxB,EAAE,6BAA6B,2CAyQ/B"}
|
|
@@ -128,9 +128,9 @@ export function ProfilePictureLibraryTab({ useLibrary, onUseLibraryChange, onPho
|
|
|
128
128
|
};
|
|
129
129
|
return (_jsxs("div", { className: "cls_profile_picture_library_tab flex flex-col gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_switch flex items-center gap-3", children: [_jsx(Switch, { id: "use-library", checked: useLibrary, onCheckedChange: onUseLibraryChange, disabled: disabled, className: "cls_profile_picture_library_tab_switch_input", "aria-label": "Use library photo" }), _jsxs(Label, { htmlFor: "use-library", className: "cls_profile_picture_library_tab_switch_label text-sm font-medium text-[var(--hazo-text-secondary)] cursor-pointer", children: ["Use library photo", _jsx(HazoUITooltip, { message: libraryTooltipMessage, iconSize: tooltipIconSizeSmall, side: "top" })] })] }), _jsxs("div", { className: "cls_profile_picture_library_tab_content grid grid-cols-12 gap-4", children: [_jsxs("div", { className: "cls_profile_picture_library_tab_categories_container flex flex-col gap-2 col-span-3", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_categories_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Categories" }), loadingCategories ? (_jsx("div", { className: "cls_profile_picture_library_tab_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : categories.length > 0 ? (_jsx(VerticalTabs, { value: selectedCategory || categories[0], onValueChange: setSelectedCategory, className: "cls_profile_picture_library_tab_vertical_tabs", children: _jsx(VerticalTabsList, { className: "cls_profile_picture_library_tab_vertical_tabs_list w-full", children: categories.map((category) => (_jsx(VerticalTabsTrigger, { value: category, className: "cls_profile_picture_library_tab_vertical_tabs_trigger w-full justify-start", children: category }, category))) }) })) : (_jsx("div", { className: "cls_profile_picture_library_tab_no_categories flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx("p", { className: "cls_profile_picture_library_tab_no_categories_text text-sm text-[var(--hazo-text-muted)]", children: "No categories available" }) }))] }), _jsxs("div", { className: "cls_profile_picture_library_tab_photos_container flex flex-col gap-2 col-span-6", children: [_jsx(Label, { className: "cls_profile_picture_library_tab_photos_label text-sm font-medium text-[var(--hazo-text-secondary)]", children: "Photos" }), loadingPhotos ? (_jsx("div", { className: "cls_profile_picture_library_tab_photos_loading flex items-center justify-center p-8 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px]", children: _jsx(Loader2, { className: "h-6 w-6 text-[var(--hazo-text-subtle)] animate-spin", "aria-hidden": "true" }) })) : photos.length > 0 ? (_jsx("div", { className: `cls_profile_picture_library_tab_photos_grid grid ${getGridColumnsClass(libraryPhotoGridColumns)} gap-3 overflow-y-auto p-4 border border-[var(--hazo-border)] rounded-lg bg-[var(--hazo-bg-subtle)] min-h-[400px] max-h-[400px]`, children: photos.map((photoUrl) => (_jsx("button", { type: "button", onClick: () => handlePhotoClick(photoUrl), className: `
|
|
130
130
|
cls_profile_picture_library_tab_photo_thumbnail
|
|
131
|
-
aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
|
|
131
|
+
w-full aspect-square rounded-lg overflow-hidden border-2 transition-colors cursor-pointer
|
|
132
132
|
${selectedPhoto === photoUrl ? "border-blue-500 ring-2 ring-blue-200" : "border-[var(--hazo-border)] hover:border-[var(--hazo-border-emphasis)]"}
|
|
133
|
-
`, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
|
|
133
|
+
`, style: { minHeight: '80px', minWidth: '80px' }, "aria-label": `Select photo ${photoUrl.split('/').pop()}`, children: _jsx("img", { src: photoUrl, alt: `Library photo ${photoUrl.split('/').pop()}`, className: "cls_profile_picture_library_tab_photo_thumbnail_image w-full h-full object-cover", loading: "lazy", onError: (e) => {
|
|
134
134
|
// Fallback if image fails to load
|
|
135
135
|
const target = e.target;
|
|
136
136
|
target.style.display = 'none';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ProfileStampTestLayoutProps = {
|
|
2
|
+
className?: string;
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* ProfileStampTestLayout - Test page for ProfileStamp component
|
|
6
|
+
* Demonstrates various scenarios and configurations
|
|
7
|
+
*/
|
|
8
|
+
export declare function ProfileStampTestLayout({ className }: ProfileStampTestLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default ProfileStampTestLayout;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/profile_stamp_test/index.tsx"],"names":[],"mappings":"AAYA,MAAM,MAAM,2BAA2B,GAAG;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAGF;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,SAAS,EAAE,EAAE,2BAA2B,2CAkNhF;AAED,eAAe,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// file_description: Test page layout for ProfileStamp component demonstrating various scenarios
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
5
|
+
// section: imports
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { ProfileStamp } from "../shared/components/profile_stamp";
|
|
8
|
+
import { use_auth_status } from "../shared/hooks/use_auth_status";
|
|
9
|
+
import { Button } from "../../ui/button";
|
|
10
|
+
import { Card } from "../../ui/card";
|
|
11
|
+
// section: component
|
|
12
|
+
/**
|
|
13
|
+
* ProfileStampTestLayout - Test page for ProfileStamp component
|
|
14
|
+
* Demonstrates various scenarios and configurations
|
|
15
|
+
*/
|
|
16
|
+
export function ProfileStampTestLayout({ className }) {
|
|
17
|
+
const authStatus = use_auth_status();
|
|
18
|
+
const [showCustomFields, setShowCustomFields] = useState(true);
|
|
19
|
+
// Sample custom fields for testing
|
|
20
|
+
const sampleCustomFields = [
|
|
21
|
+
{ label: "Role", value: "Administrator" },
|
|
22
|
+
{ label: "Department", value: "Engineering" },
|
|
23
|
+
{ label: "Joined", value: "Jan 2024" },
|
|
24
|
+
];
|
|
25
|
+
return (_jsxs("div", { className: `cls_profile_stamp_test_layout w-full max-w-4xl mx-auto p-6 ${className || ""}`, children: [_jsx("h1", { className: "text-2xl font-bold mb-6", children: "ProfileStamp Component Test" }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-2", children: "Current Auth Status" }), _jsxs("div", { className: "grid grid-cols-2 gap-2 text-sm", children: [_jsx("div", { children: "Authenticated:" }), _jsx("div", { className: authStatus.authenticated ? "text-green-600" : "text-red-600", children: authStatus.authenticated ? "Yes" : "No" }), _jsx("div", { children: "User ID:" }), _jsx("div", { children: authStatus.user_id || "N/A" }), _jsx("div", { children: "Name:" }), _jsx("div", { children: authStatus.name || "N/A" }), _jsx("div", { children: "Email:" }), _jsx("div", { children: authStatus.email || "N/A" }), _jsx("div", { children: "Profile Picture URL:" }), _jsx("div", { className: "truncate", children: authStatus.profile_picture_url || "N/A" }), _jsx("div", { children: "Profile Source:" }), _jsx("div", { children: authStatus.profile_source || "N/A" })] })] }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "Size Variants" }), _jsxs("div", { className: "flex items-end gap-6", children: [_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(ProfileStamp, { size: "sm" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "sm (24px)" })] }), _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(ProfileStamp, { size: "default" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "default (32px)" })] }), _jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx(ProfileStamp, { size: "lg" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "lg (40px)" })] })] })] }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "With Custom Fields" }), _jsx("div", { className: "flex items-center gap-4 mb-4", children: _jsx(Button, { variant: showCustomFields ? "default" : "outline", size: "sm", onClick: () => setShowCustomFields(!showCustomFields), children: showCustomFields ? "Hide Custom Fields" : "Show Custom Fields" }) }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ProfileStamp, { size: "lg", custom_fields: showCustomFields ? sampleCustomFields : [] }), _jsxs("span", { className: "text-sm text-muted-foreground", children: ["Hover to see ", showCustomFields ? "name, email, and custom fields" : "name and email only"] })] }), showCustomFields && (_jsx("div", { className: "mt-4 p-3 bg-muted rounded-md", children: _jsxs("div", { className: "text-xs font-mono", children: ["custom_fields=", JSON.stringify(sampleCustomFields, null, 2)] }) }))] }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "Display Options" }), _jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ProfileStamp, { show_name: true, show_email: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: "show_name=true, show_email=true (default)" })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ProfileStamp, { show_name: true, show_email: false }), _jsx("span", { className: "text-sm text-muted-foreground", children: "show_name=true, show_email=false" })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ProfileStamp, { show_name: false, show_email: true }), _jsx("span", { className: "text-sm text-muted-foreground", children: "show_name=false, show_email=true" })] }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx(ProfileStamp, { show_name: false, show_email: false }), _jsx("span", { className: "text-sm text-muted-foreground", children: "show_name=false, show_email=false (no hover card)" })] })] })] }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "Inline Usage Example (Note Attribution)" }), _jsx("div", { className: "border rounded-lg p-4 bg-background", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(ProfileStamp, { size: "default" }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("span", { className: "font-medium text-sm", children: authStatus.name || "User" }), _jsx("span", { className: "text-xs text-muted-foreground", children: "2 hours ago" })] }), _jsx("p", { className: "text-sm text-foreground", children: "This is an example note with a ProfileStamp component showing who added it. Hover over the profile picture to see more details about the user." })] })] }) })] }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "Comment Thread Example" }), _jsx("div", { className: "space-y-4", children: [
|
|
26
|
+
{ time: "3 hours ago", text: "Great progress on the project!" },
|
|
27
|
+
{ time: "2 hours ago", text: "I agree, the new features look amazing." },
|
|
28
|
+
{ time: "1 hour ago", text: "Let me know if you need any help with the deployment." },
|
|
29
|
+
].map((comment, index) => (_jsxs("div", { className: "flex items-start gap-3 border-b pb-4 last:border-b-0", children: [_jsx(ProfileStamp, { size: "sm", custom_fields: [{ label: "Comment", value: `#${index + 1}` }] }), _jsxs("div", { className: "flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 mb-1", children: [_jsx("span", { className: "font-medium text-sm", children: authStatus.name || "User" }), _jsx("span", { className: "text-xs text-muted-foreground", children: comment.time })] }), _jsx("p", { className: "text-sm text-foreground", children: comment.text })] })] }, index))) })] }), _jsxs(Card, { className: "p-4 mb-6", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "API Response Fields" }), _jsx("p", { className: "text-sm text-muted-foreground mb-3", children: "The /api/hazo_auth/me endpoint now returns these profile picture fields:" }), _jsx("div", { className: "p-3 bg-muted rounded-md font-mono text-xs", children: _jsx("pre", { children: `{
|
|
30
|
+
"profile_picture_url": "${authStatus.profile_picture_url || "null"}",
|
|
31
|
+
"profile_image": "${authStatus.profile_image || "null"}", // alias
|
|
32
|
+
"avatar_url": "${authStatus.avatar_url || "null"}", // alias
|
|
33
|
+
"image": "${authStatus.image || "null"}" // alias
|
|
34
|
+
}` }) })] }), _jsxs(Card, { className: "p-4", children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "Usage Code Example" }), _jsx("div", { className: "p-3 bg-muted rounded-md font-mono text-xs overflow-x-auto", children: _jsx("pre", { children: `// Basic usage
|
|
35
|
+
import { ProfileStamp } from "hazo_auth/client";
|
|
36
|
+
|
|
37
|
+
<ProfileStamp />
|
|
38
|
+
|
|
39
|
+
// With all options
|
|
40
|
+
<ProfileStamp
|
|
41
|
+
size="lg"
|
|
42
|
+
show_name={true}
|
|
43
|
+
show_email={true}
|
|
44
|
+
custom_fields={[
|
|
45
|
+
{ label: "Role", value: "Admin" },
|
|
46
|
+
{ label: "Department", value: "IT" }
|
|
47
|
+
]}
|
|
48
|
+
className="my-custom-class"
|
|
49
|
+
/>` }) })] })] }));
|
|
50
|
+
}
|
|
51
|
+
export default ProfileStampTestLayout;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type RbacTestLayoutProps = {
|
|
2
|
+
className?: string;
|
|
3
|
+
/** Whether HRBAC is enabled (passed from server) */
|
|
4
|
+
hrbacEnabled?: boolean;
|
|
5
|
+
/** Default organization for HRBAC scopes */
|
|
6
|
+
defaultOrg?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* RBAC/HRBAC Test layout component
|
|
10
|
+
* Allows testing permissions and scope access for different users
|
|
11
|
+
* @param props - Component props
|
|
12
|
+
* @returns RBAC test layout component
|
|
13
|
+
*/
|
|
14
|
+
export declare function RbacTestLayout({ className, hrbacEnabled, defaultOrg, }: RbacTestLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/rbac_test/index.tsx"],"names":[],"mappings":"AA2CA,MAAM,MAAM,mBAAmB,GAAG;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA+GF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,YAAoB,EACpB,UAAe,GAChB,EAAE,mBAAmB,2CAw3BrB"}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// file_description: RBAC/HRBAC Test layout component for testing role-based and hierarchical access control
|
|
2
|
+
// section: client_directive
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
5
|
+
// section: imports
|
|
6
|
+
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
7
|
+
import { Button } from "../../ui/button";
|
|
8
|
+
import { Label } from "../../ui/label";
|
|
9
|
+
import { Checkbox } from "../../ui/checkbox";
|
|
10
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "../../ui/card";
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../../ui/select";
|
|
12
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
|
13
|
+
import { TreeView } from "../../ui/tree-view";
|
|
14
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../../ui/avatar";
|
|
15
|
+
import { Loader2, Play, AlertCircle, CheckCircle, XCircle, Shield, Building2, FolderTree, User, RefreshCw, } from "lucide-react";
|
|
16
|
+
import { useHazoAuthConfig } from "../../../contexts/hazo_auth_provider";
|
|
17
|
+
import { use_hazo_auth } from "../shared/hooks/use_hazo_auth";
|
|
18
|
+
import { toast } from "sonner";
|
|
19
|
+
const SCOPE_LEVEL_LABELS = {
|
|
20
|
+
hazo_scopes_l1: "Level 1",
|
|
21
|
+
hazo_scopes_l2: "Level 2",
|
|
22
|
+
hazo_scopes_l3: "Level 3",
|
|
23
|
+
hazo_scopes_l4: "Level 4",
|
|
24
|
+
hazo_scopes_l5: "Level 5",
|
|
25
|
+
hazo_scopes_l6: "Level 6",
|
|
26
|
+
hazo_scopes_l7: "Level 7",
|
|
27
|
+
};
|
|
28
|
+
// Convert ScopeTreeNode to TreeDataItem format for selection
|
|
29
|
+
function convertToTreeData(nodes) {
|
|
30
|
+
return nodes.map((node) => {
|
|
31
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
32
|
+
const item = {
|
|
33
|
+
id: node.id,
|
|
34
|
+
name: `${node.name} (${node.seq})`,
|
|
35
|
+
icon: Building2,
|
|
36
|
+
scopeData: node,
|
|
37
|
+
};
|
|
38
|
+
if (hasChildren) {
|
|
39
|
+
item.children = convertToTreeData(node.children);
|
|
40
|
+
}
|
|
41
|
+
return item;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Get user initials for avatar fallback
|
|
45
|
+
function getUserInitials(user) {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
if (user.name) {
|
|
48
|
+
const parts = user.name.trim().split(" ");
|
|
49
|
+
if (parts.length >= 2) {
|
|
50
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
|
51
|
+
}
|
|
52
|
+
return ((_a = user.name[0]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || "";
|
|
53
|
+
}
|
|
54
|
+
return ((_b = user.email_address[0]) === null || _b === void 0 ? void 0 : _b.toUpperCase()) || "?";
|
|
55
|
+
}
|
|
56
|
+
// section: component
|
|
57
|
+
/**
|
|
58
|
+
* RBAC/HRBAC Test layout component
|
|
59
|
+
* Allows testing permissions and scope access for different users
|
|
60
|
+
* @param props - Component props
|
|
61
|
+
* @returns RBAC test layout component
|
|
62
|
+
*/
|
|
63
|
+
export function RbacTestLayout({ className, hrbacEnabled = false, defaultOrg = "", }) {
|
|
64
|
+
var _a;
|
|
65
|
+
const { apiBasePath } = useHazoAuthConfig();
|
|
66
|
+
const authResult = use_hazo_auth();
|
|
67
|
+
// Users state
|
|
68
|
+
const [users, setUsers] = useState([]);
|
|
69
|
+
const [usersLoading, setUsersLoading] = useState(true);
|
|
70
|
+
const [selectedUserId, setSelectedUserId] = useState("");
|
|
71
|
+
const [selectedUser, setSelectedUser] = useState(null);
|
|
72
|
+
// Selected user's permissions and scopes
|
|
73
|
+
const [userPermissions, setUserPermissions] = useState([]);
|
|
74
|
+
const [userScopes, setUserScopes] = useState([]);
|
|
75
|
+
const [userDataLoading, setUserDataLoading] = useState(false);
|
|
76
|
+
// Available permissions state
|
|
77
|
+
const [availablePermissions, setAvailablePermissions] = useState([]);
|
|
78
|
+
const [permissionsLoading, setPermissionsLoading] = useState(true);
|
|
79
|
+
// RBAC test state
|
|
80
|
+
const [selectedPermissions, setSelectedPermissions] = useState([]);
|
|
81
|
+
const [rbacTesting, setRbacTesting] = useState(false);
|
|
82
|
+
const [rbacResult, setRbacResult] = useState(null);
|
|
83
|
+
// HRBAC scope tree state
|
|
84
|
+
const [scopeTree, setScopeTree] = useState([]);
|
|
85
|
+
const [treeLoading, setTreeLoading] = useState(false);
|
|
86
|
+
const [selectedTreeItem, setSelectedTreeItem] = useState();
|
|
87
|
+
// HRBAC test state
|
|
88
|
+
const [hrbacPermissions, setHrbacPermissions] = useState([]);
|
|
89
|
+
const [hrbacTesting, setHrbacTesting] = useState(false);
|
|
90
|
+
const [hrbacResult, setHrbacResult] = useState(null);
|
|
91
|
+
// Load users
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const loadUsers = async () => {
|
|
94
|
+
var _a;
|
|
95
|
+
setUsersLoading(true);
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(`${apiBasePath}/user_management/users`);
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
if (data.success) {
|
|
100
|
+
setUsers(data.users || []);
|
|
101
|
+
// Select current user by default if available
|
|
102
|
+
if ((_a = authResult.user) === null || _a === void 0 ? void 0 : _a.id) {
|
|
103
|
+
setSelectedUserId(authResult.user.id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
toast.error("Failed to load users");
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
setUsersLoading(false);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
void loadUsers();
|
|
115
|
+
}, [apiBasePath, (_a = authResult.user) === null || _a === void 0 ? void 0 : _a.id]);
|
|
116
|
+
// Update selected user when ID changes
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (selectedUserId) {
|
|
119
|
+
const user = users.find((u) => u.id === selectedUserId);
|
|
120
|
+
setSelectedUser(user || null);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
setSelectedUser(null);
|
|
124
|
+
}
|
|
125
|
+
}, [selectedUserId, users]);
|
|
126
|
+
// Load selected user's permissions and scopes
|
|
127
|
+
const loadUserData = useCallback(async () => {
|
|
128
|
+
if (!selectedUserId) {
|
|
129
|
+
setUserPermissions([]);
|
|
130
|
+
setUserScopes([]);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
setUserDataLoading(true);
|
|
134
|
+
try {
|
|
135
|
+
// Step 1: Get user's assigned role IDs
|
|
136
|
+
const userRolesResponse = await fetch(`${apiBasePath}/user_management/users/roles?user_id=${selectedUserId}`);
|
|
137
|
+
const userRolesData = await userRolesResponse.json();
|
|
138
|
+
if (userRolesData.success && Array.isArray(userRolesData.role_ids) && userRolesData.role_ids.length > 0) {
|
|
139
|
+
// Step 2: Get all roles with their permissions
|
|
140
|
+
const rolesResponse = await fetch(`${apiBasePath}/user_management/roles`);
|
|
141
|
+
const rolesData = await rolesResponse.json();
|
|
142
|
+
if (rolesData.success && Array.isArray(rolesData.roles)) {
|
|
143
|
+
// Step 3: Filter to user's roles and extract permissions
|
|
144
|
+
const userRoleIds = new Set(userRolesData.role_ids);
|
|
145
|
+
const allPermissions = new Set();
|
|
146
|
+
for (const role of rolesData.roles) {
|
|
147
|
+
if (userRoleIds.has(role.role_id)) {
|
|
148
|
+
if (role.permissions && Array.isArray(role.permissions)) {
|
|
149
|
+
role.permissions.forEach((p) => allPermissions.add(p));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
setUserPermissions(Array.from(allPermissions));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
setUserPermissions([]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
setUserPermissions([]);
|
|
161
|
+
}
|
|
162
|
+
// Load user scopes if HRBAC is enabled
|
|
163
|
+
if (hrbacEnabled) {
|
|
164
|
+
const scopesResponse = await fetch(`${apiBasePath}/user_management/users/scopes?user_id=${selectedUserId}&include_effective=true`);
|
|
165
|
+
const scopesData = await scopesResponse.json();
|
|
166
|
+
if (scopesData.success) {
|
|
167
|
+
setUserScopes(scopesData.direct_scopes || []);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
setUserScopes([]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
toast.error("Failed to load user data");
|
|
176
|
+
setUserPermissions([]);
|
|
177
|
+
setUserScopes([]);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
setUserDataLoading(false);
|
|
181
|
+
}
|
|
182
|
+
}, [apiBasePath, selectedUserId, hrbacEnabled]);
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
void loadUserData();
|
|
185
|
+
}, [loadUserData]);
|
|
186
|
+
// Load available permissions
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const loadPermissions = async () => {
|
|
189
|
+
setPermissionsLoading(true);
|
|
190
|
+
try {
|
|
191
|
+
const response = await fetch(`${apiBasePath}/user_management/permissions`);
|
|
192
|
+
const data = await response.json();
|
|
193
|
+
if (data.success) {
|
|
194
|
+
const dbPerms = data.db_permissions.map((p) => ({
|
|
195
|
+
id: p.id,
|
|
196
|
+
permission_name: p.permission_name,
|
|
197
|
+
description: p.description,
|
|
198
|
+
source: "db",
|
|
199
|
+
}));
|
|
200
|
+
const configPerms = data.config_permissions.map((name) => ({
|
|
201
|
+
id: 0,
|
|
202
|
+
permission_name: name,
|
|
203
|
+
description: "",
|
|
204
|
+
source: "config",
|
|
205
|
+
}));
|
|
206
|
+
// Dedupe by permission_name, preferring db source
|
|
207
|
+
const permMap = new Map();
|
|
208
|
+
for (const p of [...configPerms, ...dbPerms]) {
|
|
209
|
+
permMap.set(p.permission_name, p);
|
|
210
|
+
}
|
|
211
|
+
setAvailablePermissions(Array.from(permMap.values()));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
toast.error("Failed to load permissions");
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
setPermissionsLoading(false);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
void loadPermissions();
|
|
222
|
+
}, [apiBasePath]);
|
|
223
|
+
// Load scope tree
|
|
224
|
+
const loadScopeTree = useCallback(async () => {
|
|
225
|
+
if (!hrbacEnabled)
|
|
226
|
+
return;
|
|
227
|
+
setTreeLoading(true);
|
|
228
|
+
try {
|
|
229
|
+
const params = new URLSearchParams({ action: "tree_all" });
|
|
230
|
+
const response = await fetch(`${apiBasePath}/scope_management/scopes?${params}`);
|
|
231
|
+
const data = await response.json();
|
|
232
|
+
if (data.success) {
|
|
233
|
+
setScopeTree(data.trees || []);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
setScopeTree([]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
setScopeTree([]);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
setTreeLoading(false);
|
|
244
|
+
}
|
|
245
|
+
}, [apiBasePath, hrbacEnabled]);
|
|
246
|
+
useEffect(() => {
|
|
247
|
+
void loadScopeTree();
|
|
248
|
+
}, [loadScopeTree]);
|
|
249
|
+
// Convert tree to TreeDataItem format
|
|
250
|
+
const treeData = useMemo(() => {
|
|
251
|
+
return convertToTreeData(scopeTree);
|
|
252
|
+
}, [scopeTree]);
|
|
253
|
+
// Handle tree item selection
|
|
254
|
+
const handleTreeSelectChange = (item) => {
|
|
255
|
+
setSelectedTreeItem(item);
|
|
256
|
+
};
|
|
257
|
+
// Handle RBAC permission toggle
|
|
258
|
+
const handlePermissionToggle = (permission, checked) => {
|
|
259
|
+
if (checked) {
|
|
260
|
+
setSelectedPermissions((prev) => [...prev, permission]);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
setSelectedPermissions((prev) => prev.filter((p) => p !== permission));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
// Handle HRBAC permission toggle
|
|
267
|
+
const handleHrbacPermissionToggle = (permission, checked) => {
|
|
268
|
+
if (checked) {
|
|
269
|
+
setHrbacPermissions((prev) => [...prev, permission]);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
setHrbacPermissions((prev) => prev.filter((p) => p !== permission));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
// Run RBAC test
|
|
276
|
+
const handleRunRbacTest = async () => {
|
|
277
|
+
if (!selectedUserId) {
|
|
278
|
+
toast.error("Please select a user");
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
setRbacTesting(true);
|
|
282
|
+
setRbacResult(null);
|
|
283
|
+
try {
|
|
284
|
+
const params = new URLSearchParams();
|
|
285
|
+
params.append("test_user_id", selectedUserId);
|
|
286
|
+
selectedPermissions.forEach((p) => {
|
|
287
|
+
params.append("required_permissions", p);
|
|
288
|
+
});
|
|
289
|
+
const response = await fetch(`${apiBasePath}/rbac_test?${params}`);
|
|
290
|
+
const data = await response.json();
|
|
291
|
+
setRbacResult(data);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
setRbacResult({
|
|
295
|
+
success: false,
|
|
296
|
+
authenticated: false,
|
|
297
|
+
permission_ok: false,
|
|
298
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
setRbacTesting(false);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
// Run HRBAC test
|
|
306
|
+
const handleRunHrbacTest = async () => {
|
|
307
|
+
if (!selectedUserId) {
|
|
308
|
+
toast.error("Please select a user");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!(selectedTreeItem === null || selectedTreeItem === void 0 ? void 0 : selectedTreeItem.scopeData)) {
|
|
312
|
+
toast.error("Please select a scope from the tree");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
setHrbacTesting(true);
|
|
316
|
+
setHrbacResult(null);
|
|
317
|
+
try {
|
|
318
|
+
const params = new URLSearchParams();
|
|
319
|
+
params.append("test_user_id", selectedUserId);
|
|
320
|
+
params.append("scope_type", selectedTreeItem.scopeData.level);
|
|
321
|
+
params.append("scope_id", selectedTreeItem.scopeData.id);
|
|
322
|
+
hrbacPermissions.forEach((p) => {
|
|
323
|
+
params.append("required_permissions", p);
|
|
324
|
+
});
|
|
325
|
+
const response = await fetch(`${apiBasePath}/rbac_test?${params}`);
|
|
326
|
+
const data = await response.json();
|
|
327
|
+
setHrbacResult(data);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
setHrbacResult({
|
|
331
|
+
success: false,
|
|
332
|
+
authenticated: false,
|
|
333
|
+
permission_ok: false,
|
|
334
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
setHrbacTesting(false);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
// Clear RBAC test
|
|
342
|
+
const handleClearRbac = () => {
|
|
343
|
+
setSelectedPermissions([]);
|
|
344
|
+
setRbacResult(null);
|
|
345
|
+
};
|
|
346
|
+
// Clear HRBAC test
|
|
347
|
+
const handleClearHrbac = () => {
|
|
348
|
+
setSelectedTreeItem(undefined);
|
|
349
|
+
setHrbacPermissions([]);
|
|
350
|
+
setHrbacResult(null);
|
|
351
|
+
};
|
|
352
|
+
if (authResult.loading) {
|
|
353
|
+
return (_jsx("div", { className: "cls_rbac_test_layout flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) }));
|
|
354
|
+
}
|
|
355
|
+
if (!authResult.authenticated) {
|
|
356
|
+
return (_jsxs("div", { className: "cls_rbac_test_layout flex flex-col items-center justify-center p-8 gap-4", children: [_jsx(AlertCircle, { className: "h-12 w-12 text-red-500" }), _jsx("h1", { className: "text-xl font-semibold", children: "Authentication Required" }), _jsx("p", { className: "text-muted-foreground", children: "Please log in to access the RBAC test tool." })] }));
|
|
357
|
+
}
|
|
358
|
+
// Check for admin_test_access permission
|
|
359
|
+
if (!authResult.permissions.includes("admin_test_access")) {
|
|
360
|
+
return (_jsxs("div", { className: "cls_rbac_test_layout flex flex-col items-center justify-center p-8 gap-4", children: [_jsx(Shield, { className: "h-12 w-12 text-amber-500" }), _jsx("h1", { className: "text-xl font-semibold", children: "Access Denied" }), _jsxs("p", { className: "text-muted-foreground text-center", children: ["You need the ", _jsx("code", { className: "bg-muted px-1 py-0.5 rounded", children: "admin_test_access" }), " ", "permission to use the RBAC test tool."] })] }));
|
|
361
|
+
}
|
|
362
|
+
return (_jsxs("div", { className: `cls_rbac_test_layout flex flex-col gap-6 p-4 w-full max-w-5xl mx-auto ${className || ""}`, children: [_jsxs("div", { className: "cls_rbac_test_header", children: [_jsx("h1", { className: "text-2xl font-bold", children: "RBAC & HRBAC Test" }), _jsx("p", { className: "text-muted-foreground", children: "Test Role-Based Access Control (RBAC) and Hierarchical RBAC (HRBAC) for any user." })] }), _jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "text-lg flex items-center gap-2", children: [_jsx(User, { className: "h-5 w-5" }), "Select User to Test"] }), _jsx(CardDescription, { children: "Choose a user to test their permissions and scope access" })] }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "user_select", children: "User" }), usersLoading ? (_jsxs("div", { className: "flex items-center gap-2 p-2", children: [_jsx(Loader2, { className: "h-4 w-4 animate-spin" }), _jsx("span", { className: "text-sm text-muted-foreground", children: "Loading users..." })] })) : (_jsxs(Select, { value: selectedUserId, onValueChange: setSelectedUserId, children: [_jsx(SelectTrigger, { id: "user_select", className: "w-full", children: _jsx(SelectValue, { placeholder: "Select a user" }) }), _jsx(SelectContent, { children: users.map((user) => (_jsx(SelectItem, { value: user.id, children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Avatar, { className: "h-6 w-6", children: [_jsx(AvatarImage, { src: user.profile_picture_url || undefined }), _jsx(AvatarFallback, { className: "bg-slate-200 text-slate-600 text-xs", children: getUserInitials(user) })] }), _jsx("span", { children: user.name || user.email_address }), user.name && (_jsxs("span", { className: "text-muted-foreground text-xs", children: ["(", user.email_address, ")"] }))] }) }, user.id))) })] }))] }), selectedUser && (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4 p-4 border rounded-lg bg-muted/30", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Avatar, { className: "h-12 w-12", children: [_jsx(AvatarImage, { src: selectedUser.profile_picture_url || undefined }), _jsx(AvatarFallback, { className: "bg-slate-200 text-slate-600", children: getUserInitials(selectedUser) })] }), _jsxs("div", { children: [_jsx("p", { className: "font-medium", children: selectedUser.name || selectedUser.email_address }), selectedUser.name && (_jsx("p", { className: "text-sm text-muted-foreground", children: selectedUser.email_address })), _jsxs("p", { className: "text-xs text-muted-foreground font-mono", children: [selectedUser.id.substring(0, 8), "..."] })] })] }), userDataLoading ? (_jsx("div", { className: "flex items-center justify-center", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-slate-400" }) })) : (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Permissions" }), _jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: userPermissions.length > 0 ? (userPermissions.map((p) => (_jsx("span", { className: "px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs", children: p }, p)))) : (_jsx("span", { className: "text-muted-foreground text-sm", children: "None" })) })] }), hrbacEnabled && (_jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Assigned Scopes" }), _jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: userScopes.length > 0 ? (userScopes.map((s) => (_jsx("span", { className: "px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs", title: `${SCOPE_LEVEL_LABELS[s.scope_type]}: ${s.scope_seq}`, children: s.scope_seq }, `${s.scope_type}-${s.scope_id}`)))) : (_jsx("span", { className: "text-muted-foreground text-sm", children: "None" })) })] }))] }))] }))] })] }), _jsxs(Tabs, { defaultValue: "rbac", className: "w-full", children: [_jsxs(TabsList, { className: "grid w-full grid-cols-2", children: [_jsxs(TabsTrigger, { value: "rbac", className: "flex items-center gap-2", children: [_jsx(Shield, { className: "h-4 w-4" }), "RBAC Test"] }), _jsxs(TabsTrigger, { value: "hrbac", className: "flex items-center gap-2", disabled: !hrbacEnabled, children: [_jsx(FolderTree, { className: "h-4 w-4" }), "HRBAC Test ", !hrbacEnabled && "(Disabled)"] })] }), _jsxs(TabsContent, { value: "rbac", className: "flex flex-col gap-4", children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { className: "text-lg", children: "Permission Test" }), _jsx(CardDescription, { children: "Select permissions to test if the selected user has them" })] }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [permissionsLoading ? (_jsx("div", { className: "flex items-center justify-center p-4", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-slate-400" }) })) : (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 max-h-[300px] overflow-auto p-2 border rounded", children: availablePermissions.map((perm) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `rbac_${perm.permission_name}`, checked: selectedPermissions.includes(perm.permission_name), onCheckedChange: (checked) => handlePermissionToggle(perm.permission_name, checked) }), _jsxs("label", { htmlFor: `rbac_${perm.permission_name}`, className: "text-sm cursor-pointer flex-1", children: [perm.permission_name, userPermissions.includes(perm.permission_name) && (_jsx(CheckCircle, { className: "inline h-3 w-3 text-green-500 ml-1" }))] })] }, perm.permission_name))) })), _jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [_jsxs("span", { children: ["Selected: ", selectedPermissions.length, " permission(s)"] }), selectedPermissions.length > 0 && (_jsxs("span", { className: "text-xs", children: ["(", selectedPermissions.join(", "), ")"] }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { onClick: handleRunRbacTest, disabled: rbacTesting || !selectedUserId, children: rbacTesting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Testing..."] })) : (_jsxs(_Fragment, { children: [_jsx(Play, { className: "h-4 w-4 mr-2" }), "Test Permissions"] })) }), _jsx(Button, { onClick: handleClearRbac, variant: "outline", children: "Clear" })] })] })] }), rbacResult && (_jsxs(Card, { className: rbacResult.permission_ok
|
|
363
|
+
? "border-green-200 bg-green-50/50"
|
|
364
|
+
: "border-red-200 bg-red-50/50", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-lg flex items-center gap-2", children: rbacResult.permission_ok ? (_jsxs(_Fragment, { children: [_jsx(CheckCircle, { className: "h-5 w-5 text-green-500" }), _jsx("span", { className: "text-green-700", children: "Permission Check Passed" })] })) : (_jsxs(_Fragment, { children: [_jsx(XCircle, { className: "h-5 w-5 text-red-500" }), _jsx("span", { className: "text-red-700", children: "Permission Check Failed" })] })) }) }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [rbacResult.error && (_jsx("div", { className: "bg-red-100 border border-red-200 rounded p-3", children: _jsx("p", { className: "text-red-700 text-sm", children: rbacResult.error }) })), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Authenticated" }), _jsx("p", { className: `text-sm font-medium ${rbacResult.authenticated ? "text-green-600" : "text-red-600"}`, children: rbacResult.authenticated ? "Yes" : "No" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Permission OK" }), _jsx("p", { className: `text-sm font-medium ${rbacResult.permission_ok ? "text-green-600" : "text-red-600"}`, children: rbacResult.permission_ok ? "Yes" : "No" })] })] }), rbacResult.missing_permissions && rbacResult.missing_permissions.length > 0 && (_jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Missing Permissions" }), _jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: rbacResult.missing_permissions.map((p) => (_jsx("span", { className: "px-2 py-0.5 bg-red-100 text-red-700 rounded text-xs", children: p }, p))) })] }))] })] }))] }), _jsx(TabsContent, { value: "hrbac", className: "flex flex-col gap-4", children: !hrbacEnabled ? (_jsx(Card, { children: _jsxs(CardContent, { className: "flex flex-col items-center justify-center p-8 gap-4", children: [_jsx(AlertCircle, { className: "h-12 w-12 text-amber-500" }), _jsx("h2", { className: "text-lg font-semibold", children: "HRBAC Not Enabled" }), _jsxs("p", { className: "text-muted-foreground text-center max-w-md", children: ["Enable HRBAC by setting", " ", _jsx("code", { className: "bg-muted px-1 py-0.5 rounded", children: "enable_hrbac = true" }), " in the", " ", _jsx("code", { className: "bg-muted px-1 py-0.5 rounded", children: "[hazo_auth__scope_hierarchy]" }), " ", "section."] })] }) })) : (_jsxs(_Fragment, { children: [_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsxs(CardTitle, { className: "text-lg flex items-center justify-between", children: [_jsx("span", { children: "Scope Access Test" }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void loadScopeTree(), disabled: treeLoading, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${treeLoading ? "animate-spin" : ""}` }), "Refresh"] })] }), _jsx(CardDescription, { children: "Select a scope from the tree and test if the selected user has access" })] }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Select Scope" }), treeLoading ? (_jsx("div", { className: "flex items-center justify-center p-8 border rounded-lg", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : scopeTree.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-center p-6 border rounded-lg border-dashed", children: [_jsx(FolderTree, { className: "h-8 w-8 text-muted-foreground mb-2" }), _jsx("p", { className: "text-sm text-muted-foreground text-center", children: "No scopes available. Create scopes in User Management first." })] })) : (_jsx("div", { className: "border rounded-lg max-h-[250px] overflow-auto", children: _jsx(TreeView, { data: treeData, expandAll: true, defaultNodeIcon: Building2, defaultLeafIcon: Building2, onSelectChange: handleTreeSelectChange, initialSelectedItemId: selectedTreeItem === null || selectedTreeItem === void 0 ? void 0 : selectedTreeItem.id, className: "w-full" }) }))] }), (selectedTreeItem === null || selectedTreeItem === void 0 ? void 0 : selectedTreeItem.scopeData) && (_jsxs("div", { className: "p-3 border rounded-lg bg-muted/50", children: [_jsxs("p", { className: "text-sm", children: [_jsx("span", { className: "font-medium", children: "Selected:" }), " ", selectedTreeItem.scopeData.name] }), _jsxs("p", { className: "text-xs text-muted-foreground", children: [SCOPE_LEVEL_LABELS[selectedTreeItem.scopeData.level], " -", " ", selectedTreeItem.scopeData.seq, " (", selectedTreeItem.scopeData.org, ")"] })] })), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Additional Permissions (Optional)" }), permissionsLoading ? (_jsx("div", { className: "flex items-center justify-center p-4", children: _jsx(Loader2, { className: "h-5 w-5 animate-spin text-slate-400" }) })) : (_jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 max-h-[150px] overflow-auto p-2 border rounded", children: availablePermissions.map((perm) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { id: `hrbac_${perm.permission_name}`, checked: hrbacPermissions.includes(perm.permission_name), onCheckedChange: (checked) => handleHrbacPermissionToggle(perm.permission_name, checked) }), _jsx("label", { htmlFor: `hrbac_${perm.permission_name}`, className: "text-sm cursor-pointer flex-1", children: perm.permission_name })] }, perm.permission_name))) }))] }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { onClick: handleRunHrbacTest, disabled: hrbacTesting || !selectedUserId || !(selectedTreeItem === null || selectedTreeItem === void 0 ? void 0 : selectedTreeItem.scopeData), children: hrbacTesting ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Testing..."] })) : (_jsxs(_Fragment, { children: [_jsx(Play, { className: "h-4 w-4 mr-2" }), "Test Scope Access"] })) }), _jsx(Button, { onClick: handleClearHrbac, variant: "outline", children: "Clear" })] })] })] }), hrbacResult && (_jsxs(Card, { className: hrbacResult.scope_ok
|
|
365
|
+
? "border-green-200 bg-green-50/50"
|
|
366
|
+
: "border-red-200 bg-red-50/50", children: [_jsx(CardHeader, { children: _jsx(CardTitle, { className: "text-lg flex items-center gap-2", children: hrbacResult.scope_ok ? (_jsxs(_Fragment, { children: [_jsx(CheckCircle, { className: "h-5 w-5 text-green-500" }), _jsx("span", { className: "text-green-700", children: "Scope Access Granted" })] })) : (_jsxs(_Fragment, { children: [_jsx(XCircle, { className: "h-5 w-5 text-red-500" }), _jsx("span", { className: "text-red-700", children: "Scope Access Denied" })] })) }) }), _jsxs(CardContent, { className: "flex flex-col gap-3", children: [hrbacResult.error && (_jsx("div", { className: "bg-red-100 border border-red-200 rounded p-3", children: _jsx("p", { className: "text-red-700 text-sm", children: hrbacResult.error }) })), _jsxs("div", { className: "grid grid-cols-2 md:grid-cols-4 gap-4", children: [_jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Authenticated" }), _jsx("p", { className: `text-sm font-medium ${hrbacResult.authenticated ? "text-green-600" : "text-red-600"}`, children: hrbacResult.authenticated ? "Yes" : "No" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Permission OK" }), _jsx("p", { className: `text-sm font-medium ${hrbacResult.permission_ok ? "text-green-600" : "text-red-600"}`, children: hrbacResult.permission_ok ? "Yes" : "No" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Scope OK" }), _jsx("p", { className: `text-sm font-medium ${hrbacResult.scope_ok === undefined
|
|
367
|
+
? "text-muted-foreground"
|
|
368
|
+
: hrbacResult.scope_ok
|
|
369
|
+
? "text-green-600"
|
|
370
|
+
: "text-red-600"}`, children: hrbacResult.scope_ok === undefined
|
|
371
|
+
? "N/A"
|
|
372
|
+
: hrbacResult.scope_ok
|
|
373
|
+
? "Yes"
|
|
374
|
+
: "No" })] }), _jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Access Via" }), _jsx("p", { className: "text-sm", children: hrbacResult.scope_access_via
|
|
375
|
+
? `${hrbacResult.scope_access_via.scope_seq}`
|
|
376
|
+
: "N/A" })] })] }), hrbacResult.scope_access_via && (_jsx("div", { className: "bg-green-100 border border-green-200 rounded p-3", children: _jsxs("p", { className: "text-green-700 text-sm", children: ["Access granted via scope:", " ", _jsx("strong", { children: hrbacResult.scope_access_via.scope_seq }), " (", hrbacResult.scope_access_via.scope_type, ")"] }) })), hrbacResult.missing_permissions &&
|
|
377
|
+
hrbacResult.missing_permissions.length > 0 && (_jsxs("div", { children: [_jsx(Label, { className: "text-muted-foreground text-xs", children: "Missing Permissions" }), _jsx("div", { className: "flex flex-wrap gap-1 mt-1", children: hrbacResult.missing_permissions.map((p) => (_jsx("span", { className: "px-2 py-0.5 bg-red-100 text-red-700 rounded text-xs", children: p }, p))) })] })), _jsxs("details", { className: "text-sm", children: [_jsx("summary", { className: "cursor-pointer text-muted-foreground hover:text-foreground", children: "Show raw response" }), _jsx("pre", { className: "mt-2 bg-muted p-3 rounded text-xs overflow-auto", children: JSON.stringify(hrbacResult, null, 2) })] })] })] }))] })) })] })] }));
|
|
378
|
+
}
|
|
@@ -9,5 +9,5 @@ import { Input } from "../../../ui/input";
|
|
|
9
9
|
import { FieldErrorMessage } from "./field_error_message";
|
|
10
10
|
// section: component
|
|
11
11
|
export function PasswordField({ inputId, ariaLabel, value, placeholder, autoComplete, isVisible, onChange, onToggleVisibility, errorMessage, }) {
|
|
12
|
-
return (_jsxs("div", { className: "cls_password_field_wrapper", children: [_jsxs("div", { className: "relative", children: [_jsx(Input, { id: inputId, type: isVisible ? "text" : "password", value: value, onChange: (event) => onChange(event.target.value), autoComplete: autoComplete, placeholder: placeholder, "aria-label": ariaLabel, className: "cls_password_field_input pr-11" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": `${isVisible ? "Hide" : "Show"} ${ariaLabel.toLowerCase()}`, onClick: onToggleVisibility, className: "cls_password_field_toggle absolute right-
|
|
12
|
+
return (_jsxs("div", { className: "cls_password_field_wrapper", children: [_jsxs("div", { className: "relative", children: [_jsx(Input, { id: inputId, type: isVisible ? "text" : "password", value: value, onChange: (event) => onChange(event.target.value), autoComplete: autoComplete, placeholder: placeholder, "aria-label": ariaLabel, className: "cls_password_field_input pr-11" }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": `${isVisible ? "Hide" : "Show"} ${ariaLabel.toLowerCase()}`, onClick: onToggleVisibility, className: "cls_password_field_toggle absolute right-0.5 top-1/2 h-9 w-9 -translate-y-1/2 text-slate-600 hover:bg-transparent hover:text-slate-900", children: isVisible ? (_jsx(EyeOff, { className: "h-4 w-4", "aria-hidden": "true" })) : (_jsx(Eye, { className: "h-4 w-4", "aria-hidden": "true" })) })] }), errorMessage ? (_jsx("div", { className: "mt-1 min-h-0", children: _jsx(FieldErrorMessage, { message: errorMessage }) })) : null] }));
|
|
13
13
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom field to display in the hover card
|
|
3
|
+
*/
|
|
4
|
+
export type ProfileStampCustomField = {
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Props for the ProfileStamp component
|
|
10
|
+
*/
|
|
11
|
+
export type ProfileStampProps = {
|
|
12
|
+
/**
|
|
13
|
+
* Size variant for the avatar
|
|
14
|
+
* - sm: h-6 w-6 (24px)
|
|
15
|
+
* - default: h-8 w-8 (32px)
|
|
16
|
+
* - lg: h-10 w-10 (40px)
|
|
17
|
+
*/
|
|
18
|
+
size?: "sm" | "default" | "lg";
|
|
19
|
+
/**
|
|
20
|
+
* Custom fields to display in the hover card
|
|
21
|
+
*/
|
|
22
|
+
custom_fields?: ProfileStampCustomField[];
|
|
23
|
+
/**
|
|
24
|
+
* Additional CSS classes to apply to the wrapper
|
|
25
|
+
*/
|
|
26
|
+
className?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether to show the user's name in the hover card (default: true)
|
|
29
|
+
*/
|
|
30
|
+
show_name?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to show the user's email in the hover card (default: true)
|
|
33
|
+
*/
|
|
34
|
+
show_email?: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* ProfileStamp component - displays a circular profile picture with a hover card
|
|
38
|
+
* showing the user's name, email, and any custom fields.
|
|
39
|
+
*
|
|
40
|
+
* Use this component to add profile attribution to notes, comments, or any
|
|
41
|
+
* user-generated content in your application.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Basic usage
|
|
45
|
+
* <ProfileStamp />
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // With custom fields
|
|
49
|
+
* <ProfileStamp
|
|
50
|
+
* size="lg"
|
|
51
|
+
* custom_fields={[
|
|
52
|
+
* { label: "Role", value: "Admin" },
|
|
53
|
+
* { label: "Department", value: "Engineering" }
|
|
54
|
+
* ]}
|
|
55
|
+
* />
|
|
56
|
+
*/
|
|
57
|
+
export declare function ProfileStamp({ size, custom_fields, className, show_name, show_email, }: ProfileStampProps): import("react/jsx-runtime").JSX.Element;
|
|
58
|
+
//# sourceMappingURL=profile_stamp.d.ts.map
|