hazo_auth 4.2.0 → 4.4.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/bin/hazo_auth.mjs +35 -0
- package/cli-src/assets/images/forgot_password_default.jpg +0 -0
- package/cli-src/assets/images/login_default.jpg +0 -0
- package/cli-src/assets/images/register_default.jpg +0 -0
- package/cli-src/assets/images/reset_password_default.jpg +0 -0
- package/cli-src/assets/images/verify_email_default.jpg +0 -0
- package/cli-src/cli/generate.ts +276 -0
- package/cli-src/cli/index.ts +207 -0
- package/cli-src/cli/init.ts +254 -0
- package/cli-src/cli/init_users.ts +376 -0
- package/cli-src/cli/validate.ts +581 -0
- package/cli-src/lib/already_logged_in_config.server.ts +46 -0
- package/cli-src/lib/app_logger.ts +24 -0
- package/cli-src/lib/auth/auth_cache.ts +220 -0
- package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
- package/cli-src/lib/auth/auth_types.ts +117 -0
- package/cli-src/lib/auth/auth_utils.server.ts +196 -0
- package/cli-src/lib/auth/dev_lock_validator.edge.ts +171 -0
- package/cli-src/lib/auth/hazo_get_auth.server.ts +583 -0
- package/cli-src/lib/auth/index.ts +23 -0
- package/cli-src/lib/auth/nextauth_config.ts +227 -0
- package/cli-src/lib/auth/org_cache.ts +148 -0
- package/cli-src/lib/auth/scope_cache.ts +233 -0
- package/cli-src/lib/auth/server_auth.ts +88 -0
- package/cli-src/lib/auth/session_token_validator.edge.ts +92 -0
- package/cli-src/lib/auth_utility_config.server.ts +136 -0
- package/cli-src/lib/config/config_loader.server.ts +164 -0
- package/cli-src/lib/config/default_config.ts +243 -0
- package/cli-src/lib/dev_lock_config.server.ts +148 -0
- package/cli-src/lib/email_verification_config.server.ts +63 -0
- package/cli-src/lib/file_types_config.server.ts +25 -0
- package/cli-src/lib/forgot_password_config.server.ts +63 -0
- package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
- package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
- package/cli-src/lib/hazo_connect_setup.ts +54 -0
- package/cli-src/lib/index.ts +46 -0
- package/cli-src/lib/login_config.server.ts +106 -0
- package/cli-src/lib/messages_config.server.ts +45 -0
- package/cli-src/lib/migrations/apply_migration.ts +105 -0
- package/cli-src/lib/multi_tenancy_config.server.ts +94 -0
- package/cli-src/lib/my_settings_config.server.ts +135 -0
- package/cli-src/lib/oauth_config.server.ts +87 -0
- package/cli-src/lib/password_requirements_config.server.ts +40 -0
- package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
- package/cli-src/lib/profile_picture_config.server.ts +56 -0
- package/cli-src/lib/register_config.server.ts +101 -0
- package/cli-src/lib/reset_password_config.server.ts +103 -0
- package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
- package/cli-src/lib/services/email_service.ts +587 -0
- package/cli-src/lib/services/email_verification_service.ts +270 -0
- package/cli-src/lib/services/index.ts +16 -0
- package/cli-src/lib/services/login_service.ts +150 -0
- package/cli-src/lib/services/oauth_service.ts +494 -0
- package/cli-src/lib/services/org_service.ts +965 -0
- package/cli-src/lib/services/password_change_service.ts +154 -0
- package/cli-src/lib/services/password_reset_service.ts +418 -0
- package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
- package/cli-src/lib/services/profile_picture_service.ts +451 -0
- package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
- package/cli-src/lib/services/registration_service.ts +185 -0
- package/cli-src/lib/services/scope_labels_service.ts +348 -0
- package/cli-src/lib/services/scope_service.ts +778 -0
- package/cli-src/lib/services/session_token_service.ts +178 -0
- package/cli-src/lib/services/token_service.ts +240 -0
- package/cli-src/lib/services/user_profiles_cache.ts +189 -0
- package/cli-src/lib/services/user_profiles_service.ts +264 -0
- package/cli-src/lib/services/user_scope_service.ts +554 -0
- package/cli-src/lib/services/user_update_service.ts +141 -0
- package/cli-src/lib/ui_shell_config.server.ts +73 -0
- package/cli-src/lib/ui_sizes_config.server.ts +37 -0
- package/cli-src/lib/user_fields_config.server.ts +31 -0
- package/cli-src/lib/user_management_config.server.ts +39 -0
- package/cli-src/lib/user_profiles_config.server.ts +55 -0
- package/cli-src/lib/utils/api_route_helpers.ts +60 -0
- package/cli-src/lib/utils/error_sanitizer.ts +75 -0
- package/cli-src/lib/utils/password_validator.ts +65 -0
- package/cli-src/lib/utils.ts +11 -0
- package/cli-src/server/logging/logger_service.ts +56 -0
- package/cli-src/server/types/app_types.ts +74 -0
- package/cli-src/server/types/express.d.ts +16 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/init_users.d.ts +17 -0
- package/dist/cli/init_users.d.ts.map +1 -0
- package/dist/cli/init_users.js +307 -0
- package/dist/components/layouts/dev_lock/index.d.ts +29 -0
- package/dist/components/layouts/dev_lock/index.d.ts.map +1 -0
- package/dist/components/layouts/dev_lock/index.js +60 -0
- package/dist/components/layouts/index.d.ts +2 -0
- package/dist/components/layouts/index.d.ts.map +1 -1
- package/dist/components/layouts/index.js +1 -0
- package/dist/components/layouts/org_management/index.d.ts +26 -0
- package/dist/components/layouts/org_management/index.d.ts.map +1 -0
- package/dist/components/layouts/org_management/index.js +75 -0
- package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
- package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts +13 -0
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.d.ts.map +1 -0
- package/dist/components/layouts/user_management/components/org_hierarchy_tab.js +276 -0
- package/dist/components/layouts/user_management/index.d.ts +3 -1
- package/dist/components/layouts/user_management/index.d.ts.map +1 -1
- package/dist/components/layouts/user_management/index.js +10 -4
- package/dist/lib/auth/auth_types.d.ts +6 -0
- package/dist/lib/auth/auth_types.d.ts.map +1 -1
- package/dist/lib/auth/dev_lock_validator.edge.d.ts +38 -0
- package/dist/lib/auth/dev_lock_validator.edge.d.ts.map +1 -0
- package/dist/lib/auth/dev_lock_validator.edge.js +122 -0
- package/dist/lib/auth/hazo_get_auth.server.d.ts.map +1 -1
- package/dist/lib/auth/hazo_get_auth.server.js +61 -1
- package/dist/lib/auth/org_cache.d.ts +65 -0
- package/dist/lib/auth/org_cache.d.ts.map +1 -0
- package/dist/lib/auth/org_cache.js +103 -0
- package/dist/lib/config/default_config.d.ts +76 -0
- package/dist/lib/config/default_config.d.ts.map +1 -1
- package/dist/lib/config/default_config.js +42 -0
- package/dist/lib/dev_lock_config.server.d.ts +41 -0
- package/dist/lib/dev_lock_config.server.d.ts.map +1 -0
- package/dist/lib/dev_lock_config.server.js +50 -0
- package/dist/lib/multi_tenancy_config.server.d.ts +30 -0
- package/dist/lib/multi_tenancy_config.server.d.ts.map +1 -0
- package/dist/lib/multi_tenancy_config.server.js +41 -0
- package/dist/lib/services/org_service.d.ts +191 -0
- package/dist/lib/services/org_service.d.ts.map +1 -0
- package/dist/lib/services/org_service.js +746 -0
- package/dist/lib/utils/password_validator.d.ts +7 -1
- package/dist/lib/utils/password_validator.d.ts.map +1 -1
- package/dist/page_components/dev_lock.d.ts +11 -0
- package/dist/page_components/dev_lock.d.ts.map +1 -0
- package/dist/page_components/dev_lock.js +17 -0
- package/dist/page_components/index.d.ts +1 -0
- package/dist/page_components/index.d.ts.map +1 -1
- package/dist/page_components/index.js +1 -0
- package/dist/page_components/org_management.d.ts +27 -0
- package/dist/page_components/org_management.d.ts.map +1 -0
- package/dist/page_components/org_management.js +18 -0
- package/hazo_auth_config.example.ini +30 -0
- package/package.json +27 -3
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// file_description: Organization Hierarchy tab component for managing multi-tenancy organizations using tree view
|
|
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, useCallback, useMemo } from "react";
|
|
7
|
+
import { TreeView } from "../../../ui/tree-view";
|
|
8
|
+
import { Button } from "../../../ui/button";
|
|
9
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../ui/dialog";
|
|
10
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../../../ui/alert-dialog";
|
|
11
|
+
import { Input } from "../../../ui/input";
|
|
12
|
+
import { Label } from "../../../ui/label";
|
|
13
|
+
import { Loader2, Plus, Edit, Trash2, CircleCheck, CircleX, Building2, FolderTree, RefreshCw, AlertCircle, } from "lucide-react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
import { useHazoAuthConfig } from "../../../../contexts/hazo_auth_provider";
|
|
16
|
+
// section: helpers
|
|
17
|
+
function getUserCountDisplay(org) {
|
|
18
|
+
var _a;
|
|
19
|
+
const count = (_a = org.current_user_count) !== null && _a !== void 0 ? _a : 0;
|
|
20
|
+
if (org.user_limit === 0) {
|
|
21
|
+
return `${count} users`;
|
|
22
|
+
}
|
|
23
|
+
return `${count}/${org.user_limit} users`;
|
|
24
|
+
}
|
|
25
|
+
// Convert OrgTreeNode to TreeDataItem format
|
|
26
|
+
function convertToTreeData(nodes, onEdit, onDelete, onAddChild) {
|
|
27
|
+
return nodes.map((node) => {
|
|
28
|
+
const hasChildren = node.children && node.children.length > 0;
|
|
29
|
+
const isInactive = node.active === false;
|
|
30
|
+
// Build display name with user count and status
|
|
31
|
+
const displayName = isInactive
|
|
32
|
+
? `${node.name} (${getUserCountDisplay(node)}) [Inactive]`
|
|
33
|
+
: `${node.name} (${getUserCountDisplay(node)})`;
|
|
34
|
+
const item = {
|
|
35
|
+
id: node.id,
|
|
36
|
+
name: displayName,
|
|
37
|
+
icon: Building2,
|
|
38
|
+
orgData: node,
|
|
39
|
+
className: isInactive ? "text-muted-foreground line-through" : undefined,
|
|
40
|
+
actions: (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: (e) => {
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
onAddChild(node);
|
|
43
|
+
}, title: "Add child organization", children: _jsx(Plus, { className: "h-3 w-3" }) }), _jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0", onClick: (e) => {
|
|
44
|
+
e.stopPropagation();
|
|
45
|
+
onEdit(node);
|
|
46
|
+
}, title: "Edit organization", children: _jsx(Edit, { className: "h-3 w-3" }) }), node.active !== false && (_jsx(Button, { variant: "ghost", size: "sm", className: "h-6 w-6 p-0 text-destructive hover:text-destructive", onClick: (e) => {
|
|
47
|
+
e.stopPropagation();
|
|
48
|
+
onDelete(node);
|
|
49
|
+
}, title: "Deactivate organization", children: _jsx(Trash2, { className: "h-3 w-3" }) }))] })),
|
|
50
|
+
};
|
|
51
|
+
if (hasChildren) {
|
|
52
|
+
item.children = convertToTreeData(node.children, onEdit, onDelete, onAddChild);
|
|
53
|
+
}
|
|
54
|
+
return item;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// section: component
|
|
58
|
+
/**
|
|
59
|
+
* Organization Hierarchy tab component for managing multi-tenancy organizations
|
|
60
|
+
* Displays organizations in a tree view for intuitive hierarchy configuration
|
|
61
|
+
* @param props - Component props
|
|
62
|
+
* @returns Organization Hierarchy tab component
|
|
63
|
+
*/
|
|
64
|
+
export function OrgHierarchyTab({ className, isGlobalAdmin = false, }) {
|
|
65
|
+
const { apiBasePath } = useHazoAuthConfig();
|
|
66
|
+
// State
|
|
67
|
+
const [tree, setTree] = useState([]);
|
|
68
|
+
const [loading, setLoading] = useState(true);
|
|
69
|
+
const [actionLoading, setActionLoading] = useState(false);
|
|
70
|
+
const [selectedItem, setSelectedItem] = useState();
|
|
71
|
+
const [showInactive, setShowInactive] = useState(false);
|
|
72
|
+
// Dialog state
|
|
73
|
+
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
74
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
|
75
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
76
|
+
const [selectedOrg, setSelectedOrg] = useState(null);
|
|
77
|
+
const [addParentOrg, setAddParentOrg] = useState(null);
|
|
78
|
+
// Form state
|
|
79
|
+
const [newName, setNewName] = useState("");
|
|
80
|
+
const [newUserLimit, setNewUserLimit] = useState(0);
|
|
81
|
+
const [editName, setEditName] = useState("");
|
|
82
|
+
const [editUserLimit, setEditUserLimit] = useState(0);
|
|
83
|
+
// Load tree data
|
|
84
|
+
const loadTree = useCallback(async () => {
|
|
85
|
+
setLoading(true);
|
|
86
|
+
try {
|
|
87
|
+
const params = new URLSearchParams({
|
|
88
|
+
action: "tree",
|
|
89
|
+
include_inactive: showInactive.toString(),
|
|
90
|
+
});
|
|
91
|
+
const response = await fetch(`${apiBasePath}/org_management/orgs?${params}`);
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
if (data.success) {
|
|
94
|
+
setTree(data.tree || []);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (data.code === "MULTI_TENANCY_DISABLED") {
|
|
98
|
+
toast.error("Multi-tenancy is not enabled");
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
toast.error(data.error || "Failed to load organization hierarchy");
|
|
102
|
+
}
|
|
103
|
+
setTree([]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
toast.error("Failed to load organization hierarchy");
|
|
108
|
+
setTree([]);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
setLoading(false);
|
|
112
|
+
}
|
|
113
|
+
}, [apiBasePath, showInactive]);
|
|
114
|
+
// Load data on mount and when showInactive changes
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
void loadTree();
|
|
117
|
+
}, [loadTree]);
|
|
118
|
+
// Handle add org (root level) - only for global admins
|
|
119
|
+
const handleAddRootOrg = () => {
|
|
120
|
+
if (!isGlobalAdmin) {
|
|
121
|
+
toast.error("Only global admins can create root organizations");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
setAddParentOrg(null);
|
|
125
|
+
setNewName("");
|
|
126
|
+
setNewUserLimit(0);
|
|
127
|
+
setAddDialogOpen(true);
|
|
128
|
+
};
|
|
129
|
+
// Handle add child org
|
|
130
|
+
const handleAddChildOrg = (parent) => {
|
|
131
|
+
setAddParentOrg(parent);
|
|
132
|
+
setNewName("");
|
|
133
|
+
setNewUserLimit(0);
|
|
134
|
+
setAddDialogOpen(true);
|
|
135
|
+
};
|
|
136
|
+
// Handle edit org
|
|
137
|
+
const openEditDialog = (org) => {
|
|
138
|
+
setSelectedOrg(org);
|
|
139
|
+
setEditName(org.name);
|
|
140
|
+
setEditUserLimit(org.user_limit || 0);
|
|
141
|
+
setEditDialogOpen(true);
|
|
142
|
+
};
|
|
143
|
+
// Handle delete org
|
|
144
|
+
const openDeleteDialog = (org) => {
|
|
145
|
+
setSelectedOrg(org);
|
|
146
|
+
setDeleteDialogOpen(true);
|
|
147
|
+
};
|
|
148
|
+
// Create org
|
|
149
|
+
const handleCreateOrg = async () => {
|
|
150
|
+
if (!newName.trim()) {
|
|
151
|
+
toast.error("Name is required");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
setActionLoading(true);
|
|
155
|
+
try {
|
|
156
|
+
const body = {
|
|
157
|
+
name: newName.trim(),
|
|
158
|
+
user_limit: newUserLimit,
|
|
159
|
+
};
|
|
160
|
+
if (addParentOrg) {
|
|
161
|
+
body.parent_org_id = addParentOrg.id;
|
|
162
|
+
}
|
|
163
|
+
const response = await fetch(`${apiBasePath}/org_management/orgs`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
body: JSON.stringify(body),
|
|
167
|
+
});
|
|
168
|
+
const data = await response.json();
|
|
169
|
+
if (data.success) {
|
|
170
|
+
toast.success("Organization created successfully");
|
|
171
|
+
setAddDialogOpen(false);
|
|
172
|
+
setNewName("");
|
|
173
|
+
setNewUserLimit(0);
|
|
174
|
+
setAddParentOrg(null);
|
|
175
|
+
await loadTree();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
toast.error(data.error || "Failed to create organization");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
toast.error("Failed to create organization");
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
setActionLoading(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
// Update org
|
|
189
|
+
const handleUpdateOrg = async () => {
|
|
190
|
+
if (!selectedOrg)
|
|
191
|
+
return;
|
|
192
|
+
if (!editName.trim()) {
|
|
193
|
+
toast.error("Name is required");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setActionLoading(true);
|
|
197
|
+
try {
|
|
198
|
+
const body = {
|
|
199
|
+
org_id: selectedOrg.id,
|
|
200
|
+
name: editName.trim(),
|
|
201
|
+
user_limit: editUserLimit,
|
|
202
|
+
};
|
|
203
|
+
const response = await fetch(`${apiBasePath}/org_management/orgs`, {
|
|
204
|
+
method: "PATCH",
|
|
205
|
+
headers: { "Content-Type": "application/json" },
|
|
206
|
+
body: JSON.stringify(body),
|
|
207
|
+
});
|
|
208
|
+
const data = await response.json();
|
|
209
|
+
if (data.success) {
|
|
210
|
+
toast.success("Organization updated successfully");
|
|
211
|
+
setEditDialogOpen(false);
|
|
212
|
+
setSelectedOrg(null);
|
|
213
|
+
setEditName("");
|
|
214
|
+
setEditUserLimit(0);
|
|
215
|
+
await loadTree();
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
toast.error(data.error || "Failed to update organization");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
toast.error("Failed to update organization");
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
setActionLoading(false);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
// Delete (deactivate) org
|
|
229
|
+
const handleDeleteOrg = async () => {
|
|
230
|
+
if (!selectedOrg)
|
|
231
|
+
return;
|
|
232
|
+
setActionLoading(true);
|
|
233
|
+
try {
|
|
234
|
+
const params = new URLSearchParams({
|
|
235
|
+
org_id: selectedOrg.id,
|
|
236
|
+
});
|
|
237
|
+
const response = await fetch(`${apiBasePath}/org_management/orgs?${params}`, {
|
|
238
|
+
method: "DELETE",
|
|
239
|
+
});
|
|
240
|
+
const data = await response.json();
|
|
241
|
+
if (data.success) {
|
|
242
|
+
toast.success("Organization deactivated successfully");
|
|
243
|
+
setDeleteDialogOpen(false);
|
|
244
|
+
setSelectedOrg(null);
|
|
245
|
+
await loadTree();
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
toast.error(data.error || "Failed to deactivate organization");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
toast.error("Failed to deactivate organization");
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
setActionLoading(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
// Convert tree to TreeDataItem format
|
|
259
|
+
const treeData = useMemo(() => {
|
|
260
|
+
return convertToTreeData(tree, openEditDialog, openDeleteDialog, handleAddChildOrg);
|
|
261
|
+
}, [tree]);
|
|
262
|
+
// Handle tree item selection
|
|
263
|
+
const handleSelectChange = (item) => {
|
|
264
|
+
setSelectedItem(item);
|
|
265
|
+
};
|
|
266
|
+
return (_jsxs("div", { className: `cls_org_hierarchy_tab flex flex-col gap-4 w-full ${className || ""}`, children: [_jsxs("div", { className: "cls_org_hierarchy_header flex items-center justify-between gap-4 flex-wrap", children: [_jsxs("div", { className: "cls_org_hierarchy_header_left flex items-center gap-4", children: [_jsxs("div", { className: "cls_org_hierarchy_inactive_toggle flex items-center gap-2", children: [_jsx("input", { type: "checkbox", id: "show_inactive", checked: showInactive, onChange: (e) => setShowInactive(e.target.checked), className: "h-4 w-4 rounded border-gray-300" }), _jsx(Label, { htmlFor: "show_inactive", className: "text-sm font-medium cursor-pointer", children: "Show inactive" })] }), _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void loadTree(), disabled: loading, children: [_jsx(RefreshCw, { className: `h-4 w-4 mr-2 ${loading ? "animate-spin" : ""}` }), "Refresh"] })] }), _jsx("div", { className: "cls_org_hierarchy_header_right", children: isGlobalAdmin && (_jsxs(Button, { onClick: handleAddRootOrg, variant: "default", size: "sm", children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Add Root Organization"] })) })] }), loading ? (_jsx("div", { className: "cls_org_hierarchy_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : tree.length === 0 ? (_jsxs("div", { className: "cls_org_hierarchy_empty flex flex-col items-center justify-center p-8 border rounded-lg border-dashed", children: [_jsx(FolderTree, { className: "h-12 w-12 text-muted-foreground mb-4" }), _jsx("p", { className: "text-muted-foreground text-center mb-4", children: "No organizations found" }), isGlobalAdmin && (_jsxs(Button, { onClick: handleAddRootOrg, variant: "outline", size: "sm", children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Create First Organization"] }))] })) : (_jsx("div", { className: "cls_org_hierarchy_tree_container border rounded-lg overflow-auto w-full min-h-[300px]", children: _jsx(TreeView, { data: treeData, expandAll: true, defaultNodeIcon: Building2, defaultLeafIcon: Building2, onSelectChange: handleSelectChange, className: "w-full" }) })), (selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.orgData) && (_jsxs("div", { className: "cls_org_hierarchy_selected_info p-4 border rounded-lg bg-muted/50", children: [_jsx("h4", { className: "font-medium mb-2", children: "Selected Organization" }), _jsxs("div", { className: "grid grid-cols-2 gap-2 text-sm", children: [_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Name:" }), " ", selectedItem.orgData.name] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Users:" }), " ", getUserCountDisplay(selectedItem.orgData)] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "Status:" }), " ", selectedItem.orgData.active === false ? (_jsx("span", { className: "text-destructive", children: "Inactive" })) : (_jsx("span", { className: "text-green-600", children: "Active" }))] }), _jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "ID:" }), " ", _jsxs("span", { className: "font-mono text-xs", children: [selectedItem.orgData.id.slice(0, 8), "..."] })] })] })] })), _jsx(Dialog, { open: addDialogOpen, onOpenChange: setAddDialogOpen, children: _jsxs(DialogContent, { className: "cls_org_hierarchy_add_dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: addParentOrg
|
|
267
|
+
? `Add Child Organization to "${addParentOrg.name}"`
|
|
268
|
+
: "Add Root Organization" }), _jsxs(DialogDescription, { children: ["Create a new organization", addParentOrg &&
|
|
269
|
+
` as a child of "${addParentOrg.name}".`] })] }), _jsxs("div", { className: "flex flex-col gap-4 py-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "new_org_name", children: "Name *" }), _jsx(Input, { id: "new_org_name", value: newName, onChange: (e) => setNewName(e.target.value), placeholder: "Enter organization name" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "new_org_user_limit", children: "User Limit (0 = unlimited)" }), _jsx(Input, { id: "new_org_user_limit", type: "number", min: 0, value: newUserLimit, onChange: (e) => setNewUserLimit(parseInt(e.target.value) || 0), placeholder: "0" })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: handleCreateOrg, disabled: actionLoading, variant: "default", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Creating..."] })) : (_jsxs(_Fragment, { children: [_jsx(CircleCheck, { className: "h-4 w-4 mr-2" }), "Create"] })) }), _jsxs(Button, { onClick: () => setAddDialogOpen(false), variant: "outline", children: [_jsx(CircleX, { className: "h-4 w-4 mr-2" }), "Cancel"] })] })] }) }), _jsx(Dialog, { open: editDialogOpen, onOpenChange: setEditDialogOpen, children: _jsxs(DialogContent, { className: "cls_org_hierarchy_edit_dialog", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Edit Organization" }), _jsxs(DialogDescription, { children: ["Update organization: ", selectedOrg === null || selectedOrg === void 0 ? void 0 : selectedOrg.name] })] }), _jsxs("div", { className: "flex flex-col gap-4 py-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "edit_org_name", children: "Name *" }), _jsx(Input, { id: "edit_org_name", value: editName, onChange: (e) => setEditName(e.target.value), placeholder: "Enter organization name" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "edit_org_user_limit", children: "User Limit (0 = unlimited)" }), _jsx(Input, { id: "edit_org_user_limit", type: "number", min: 0, value: editUserLimit, onChange: (e) => setEditUserLimit(parseInt(e.target.value) || 0), placeholder: "0" })] })] }), _jsxs(DialogFooter, { children: [_jsx(Button, { onClick: handleUpdateOrg, disabled: actionLoading, variant: "default", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(CircleCheck, { className: "h-4 w-4 mr-2" }), "Save"] })) }), _jsxs(Button, { onClick: () => {
|
|
270
|
+
setEditDialogOpen(false);
|
|
271
|
+
setSelectedOrg(null);
|
|
272
|
+
}, variant: "outline", children: [_jsx(CircleX, { className: "h-4 w-4 mr-2" }), "Cancel"] })] })] }) }), _jsx(AlertDialog, { open: deleteDialogOpen, onOpenChange: setDeleteDialogOpen, children: _jsxs(AlertDialogContent, { children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Deactivate Organization" }), _jsx(AlertDialogDescription, { children: _jsxs("div", { className: "flex items-start gap-2", children: [_jsx(AlertCircle, { className: "h-5 w-5 text-amber-500 mt-0.5 flex-shrink-0" }), _jsxs("span", { children: ["Are you sure you want to deactivate \"", selectedOrg === null || selectedOrg === void 0 ? void 0 : selectedOrg.name, "\"? This will mark the organization as inactive but will not delete it. Users in this organization may lose access."] })] }) })] }), _jsxs(AlertDialogFooter, { children: [_jsx(AlertDialogAction, { onClick: handleDeleteOrg, disabled: actionLoading, className: "bg-destructive text-destructive-foreground hover:bg-destructive/90", children: actionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Deactivating..."] })) : ("Deactivate") }), _jsx(AlertDialogCancel, { onClick: () => {
|
|
273
|
+
setDeleteDialogOpen(false);
|
|
274
|
+
setSelectedOrg(null);
|
|
275
|
+
}, children: "Cancel" })] })] }) })] }));
|
|
276
|
+
}
|
|
@@ -2,6 +2,8 @@ export type UserManagementLayoutProps = {
|
|
|
2
2
|
className?: string;
|
|
3
3
|
/** Whether HRBAC is enabled (passed from server) */
|
|
4
4
|
hrbacEnabled?: boolean;
|
|
5
|
+
/** Whether multi-tenancy is enabled (passed from server) */
|
|
6
|
+
multiTenancyEnabled?: boolean;
|
|
5
7
|
/** Default organization for HRBAC scopes */
|
|
6
8
|
defaultOrg?: string;
|
|
7
9
|
};
|
|
@@ -16,5 +18,5 @@ export type UserManagementLayoutProps = {
|
|
|
16
18
|
* @param props - Component props
|
|
17
19
|
* @returns User Management layout component
|
|
18
20
|
*/
|
|
19
|
-
export declare function UserManagementLayout({ className, hrbacEnabled, defaultOrg }: UserManagementLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function UserManagementLayout({ className, hrbacEnabled, multiTenancyEnabled, defaultOrg }: UserManagementLayoutProps): import("react/jsx-runtime").JSX.Element;
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/user_management/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/user_management/index.tsx"],"names":[],"mappings":"AAiDA,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,4DAA4D;IAC5D,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAsBF;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,YAAoB,EAAE,mBAA2B,EAAE,UAAe,EAAE,EAAE,yBAAyB,2CAkwChJ"}
|
|
@@ -17,6 +17,7 @@ import { RolesMatrix } from "./components/roles_matrix";
|
|
|
17
17
|
import { ScopeHierarchyTab } from "./components/scope_hierarchy_tab";
|
|
18
18
|
import { ScopeLabelsTab } from "./components/scope_labels_tab";
|
|
19
19
|
import { UserScopesTab } from "./components/user_scopes_tab";
|
|
20
|
+
import { OrgHierarchyTab } from "./components/org_hierarchy_tab";
|
|
20
21
|
import { UserX, KeyRound, Edit, Trash2, Loader2, CircleCheck, CircleX, Plus, UserPlus } from "lucide-react";
|
|
21
22
|
import { toast } from "sonner";
|
|
22
23
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../../ui/tooltip";
|
|
@@ -33,7 +34,7 @@ import { useHazoAuthConfig } from "../../../contexts/hazo_auth_provider";
|
|
|
33
34
|
* @param props - Component props
|
|
34
35
|
* @returns User Management layout component
|
|
35
36
|
*/
|
|
36
|
-
export function UserManagementLayout({ className, hrbacEnabled = false, defaultOrg = "" }) {
|
|
37
|
+
export function UserManagementLayout({ className, hrbacEnabled = false, multiTenancyEnabled = false, defaultOrg = "" }) {
|
|
37
38
|
const { apiBasePath } = useHazoAuthConfig();
|
|
38
39
|
// Permission checks
|
|
39
40
|
const authResult = use_hazo_auth();
|
|
@@ -47,6 +48,10 @@ export function UserManagementLayout({ className, hrbacEnabled = false, defaultO
|
|
|
47
48
|
authResult.permissions.includes("admin_scope_hierarchy_management");
|
|
48
49
|
const hasUserScopeAssignmentPermission = authResult.authenticated &&
|
|
49
50
|
authResult.permissions.includes("admin_user_scope_assignment");
|
|
51
|
+
const hasOrgManagementPermission = authResult.authenticated &&
|
|
52
|
+
authResult.permissions.includes("hazo_perm_org_management");
|
|
53
|
+
const hasOrgGlobalAdminPermission = authResult.authenticated &&
|
|
54
|
+
authResult.permissions.includes("hazo_org_global_admin");
|
|
50
55
|
// Determine which tabs to show
|
|
51
56
|
const showUsersTab = hasUserManagementPermission;
|
|
52
57
|
const showRolesTab = hasRoleManagementPermission;
|
|
@@ -54,7 +59,8 @@ export function UserManagementLayout({ className, hrbacEnabled = false, defaultO
|
|
|
54
59
|
const showScopeHierarchyTab = hrbacEnabled && hasScopeHierarchyPermission;
|
|
55
60
|
const showScopeLabelsTab = hrbacEnabled && hasScopeHierarchyPermission;
|
|
56
61
|
const showUserScopesTab = hrbacEnabled && hasUserScopeAssignmentPermission;
|
|
57
|
-
const
|
|
62
|
+
const showOrgsTab = multiTenancyEnabled && hasOrgManagementPermission;
|
|
63
|
+
const hasAnyPermission = showUsersTab || showRolesTab || showPermissionsTab || showScopeHierarchyTab || showScopeLabelsTab || showUserScopesTab || showOrgsTab;
|
|
58
64
|
// Tab 1: Users state
|
|
59
65
|
const [users, setUsers] = useState([]);
|
|
60
66
|
const [usersLoading, setUsersLoading] = useState(true);
|
|
@@ -436,7 +442,7 @@ export function UserManagementLayout({ className, hrbacEnabled = false, defaultO
|
|
|
436
442
|
showPermissionsTab ? "permissions" :
|
|
437
443
|
showScopeLabelsTab ? "scope_labels" :
|
|
438
444
|
showScopeHierarchyTab ? "scope_hierarchy" :
|
|
439
|
-
showUserScopesTab ? "user_scopes" : "users", className: "cls_user_management_tabs w-full", children: [_jsxs(TabsList, { className: "cls_user_management_tabs_list flex w-full flex-wrap", children: [showUsersTab && (_jsx(TabsTrigger, { value: "users", className: "cls_user_management_tabs_trigger flex-1", children: "Manage Users" })), showRolesTab && (_jsx(TabsTrigger, { value: "roles", className: "cls_user_management_tabs_trigger flex-1", children: "Roles" })), showPermissionsTab && (_jsx(TabsTrigger, { value: "permissions", className: "cls_user_management_tabs_trigger flex-1", children: "Permissions" })), showScopeLabelsTab && (_jsx(TabsTrigger, { value: "scope_labels", className: "cls_user_management_tabs_trigger flex-1", children: "Scope Labels" })), showScopeHierarchyTab && (_jsx(TabsTrigger, { value: "scope_hierarchy", className: "cls_user_management_tabs_trigger flex-1", children: "Scope Hierarchy" })), showUserScopesTab && (_jsx(TabsTrigger, { value: "user_scopes", className: "cls_user_management_tabs_trigger flex-1", children: "User Scopes" }))] }), showUsersTab && (_jsx(TabsContent, { value: "users", className: "cls_user_management_tab_users w-full", children: usersLoading ? (_jsx("div", { className: "cls_user_management_users_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : (_jsx("div", { className: "cls_user_management_users_table_container border rounded-lg overflow-auto w-full", children: _jsxs(Table, { className: "cls_user_management_users_table w-full", children: [_jsx(TableHeader, { className: "cls_user_management_users_table_header", children: _jsxs(TableRow, { className: "cls_user_management_users_table_header_row", children: [_jsx(TableHead, { className: "cls_user_management_users_table_header_profile_pic w-16", children: "Photo" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_id", children: "ID" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_name", children: "Name" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_email", children: "Email" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_email_verified", children: "Email Verified" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_is_active", children: "Active" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_last_logon", children: "Last Logon" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_created_at", children: "Created At" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_actions text-right", children: "Actions" })] }) }), _jsx(TableBody, { className: "cls_user_management_users_table_body", children: users.length === 0 ? (_jsx(TableRow, { className: "cls_user_management_users_table_row_empty", children: _jsx(TableCell, { colSpan: 9, className: "text-center text-muted-foreground py-8", children: "No users found." }) })) : (users.map((user) => (_jsxs(TableRow, { className: "cls_user_management_users_table_row cursor-pointer hover:bg-muted/50", onClick: () => {
|
|
445
|
+
showUserScopesTab ? "user_scopes" : "users", className: "cls_user_management_tabs w-full", children: [_jsxs(TabsList, { className: "cls_user_management_tabs_list flex w-full flex-wrap", children: [showUsersTab && (_jsx(TabsTrigger, { value: "users", className: "cls_user_management_tabs_trigger flex-1", children: "Manage Users" })), showRolesTab && (_jsx(TabsTrigger, { value: "roles", className: "cls_user_management_tabs_trigger flex-1", children: "Roles" })), showPermissionsTab && (_jsx(TabsTrigger, { value: "permissions", className: "cls_user_management_tabs_trigger flex-1", children: "Permissions" })), showScopeLabelsTab && (_jsx(TabsTrigger, { value: "scope_labels", className: "cls_user_management_tabs_trigger flex-1", children: "Scope Labels" })), showScopeHierarchyTab && (_jsx(TabsTrigger, { value: "scope_hierarchy", className: "cls_user_management_tabs_trigger flex-1", children: "Scope Hierarchy" })), showUserScopesTab && (_jsx(TabsTrigger, { value: "user_scopes", className: "cls_user_management_tabs_trigger flex-1", children: "User Scopes" })), showOrgsTab && (_jsx(TabsTrigger, { value: "organizations", className: "cls_user_management_tabs_trigger flex-1", children: "Organizations" }))] }), showUsersTab && (_jsx(TabsContent, { value: "users", className: "cls_user_management_tab_users w-full", children: usersLoading ? (_jsx("div", { className: "cls_user_management_users_loading flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : (_jsx("div", { className: "cls_user_management_users_table_container border rounded-lg overflow-auto w-full", children: _jsxs(Table, { className: "cls_user_management_users_table w-full", children: [_jsx(TableHeader, { className: "cls_user_management_users_table_header", children: _jsxs(TableRow, { className: "cls_user_management_users_table_header_row", children: [_jsx(TableHead, { className: "cls_user_management_users_table_header_profile_pic w-16", children: "Photo" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_id", children: "ID" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_name", children: "Name" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_email", children: "Email" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_email_verified", children: "Email Verified" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_is_active", children: "Active" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_last_logon", children: "Last Logon" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_created_at", children: "Created At" }), _jsx(TableHead, { className: "cls_user_management_users_table_header_actions text-right", children: "Actions" })] }) }), _jsx(TableBody, { className: "cls_user_management_users_table_body", children: users.length === 0 ? (_jsx(TableRow, { className: "cls_user_management_users_table_row_empty", children: _jsx(TableCell, { colSpan: 9, className: "text-center text-muted-foreground py-8", children: "No users found." }) })) : (users.map((user) => (_jsxs(TableRow, { className: "cls_user_management_users_table_row cursor-pointer hover:bg-muted/50", onClick: () => {
|
|
440
446
|
setSelectedUser(user);
|
|
441
447
|
setUserDetailDialogOpen(true);
|
|
442
448
|
}, children: [_jsx(TableCell, { className: "cls_user_management_users_table_cell_profile_pic", children: _jsxs(Avatar, { className: "cls_user_management_users_table_avatar h-8 w-8", children: [_jsx(AvatarImage, { src: user.profile_picture_url || undefined, alt: user.name ? `Profile picture of ${user.name}` : "Profile picture", className: "cls_user_management_users_table_avatar_image" }), _jsx(AvatarFallback, { className: "cls_user_management_users_table_avatar_fallback bg-slate-200 text-slate-600 text-xs", children: getUserInitials(user) })] }) }), _jsxs(TableCell, { className: "cls_user_management_users_table_cell_id font-mono text-xs", children: [user.id.substring(0, 8), "..."] }), _jsx(TableCell, { className: "cls_user_management_users_table_cell_name", children: user.name || "-" }), _jsx(TableCell, { className: "cls_user_management_users_table_cell_email", children: user.email_address }), _jsx(TableCell, { className: "cls_user_management_users_table_cell_email_verified", children: user.email_verified ? (_jsx("span", { className: "text-green-600", children: "Yes" })) : (_jsx("span", { className: "text-red-600", children: "No" })) }), _jsx(TableCell, { className: "cls_user_management_users_table_cell_is_active", children: user.is_active ? (_jsx("span", { className: "text-green-600", children: "Active" })) : (_jsx("span", { className: "text-red-600", children: "Inactive" })) }), _jsx(TableCell, { className: "cls_user_management_users_table_cell_last_logon", children: user.last_logon
|
|
@@ -461,7 +467,7 @@ export function UserManagementLayout({ className, hrbacEnabled = false, defaultO
|
|
|
461
467
|
setEditingPermission(permission);
|
|
462
468
|
setEditDescription(permission.description);
|
|
463
469
|
setEditPermissionDialogOpen(true);
|
|
464
|
-
}, variant: "outline", size: "sm", className: "cls_user_management_permissions_table_action_edit", children: [_jsx(Edit, { className: "h-4 w-4 mr-1" }), "Edit"] }), _jsxs(Button, { onClick: () => handleDeletePermission(permission), disabled: permissionsActionLoading, variant: "outline", size: "sm", className: "cls_user_management_permissions_table_action_delete text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-1" }), "Delete"] })] })) }) })] }, `${permission.source}-${permission.id}-${permission.permission_name}`)))) })] }) }))] }) })), showScopeLabelsTab && (_jsx(TabsContent, { value: "scope_labels", className: "cls_user_management_tab_scope_labels w-full", children: _jsx(ScopeLabelsTab, { defaultOrg: defaultOrg }) })), showScopeHierarchyTab && (_jsx(TabsContent, { value: "scope_hierarchy", className: "cls_user_management_tab_scope_hierarchy w-full", children: _jsx(ScopeHierarchyTab, { defaultOrg: defaultOrg }) })), showUserScopesTab && (_jsx(TabsContent, { value: "user_scopes", className: "cls_user_management_tab_user_scopes w-full", children: _jsx(UserScopesTab, {}) }))] })), _jsx(AlertDialog, { open: deactivateDialogOpen, onOpenChange: setDeactivateDialogOpen, children: _jsxs(AlertDialogContent, { className: "cls_user_management_deactivate_dialog", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Deactivate User" }), _jsxs(AlertDialogDescription, { children: ["Are you sure you want to deactivate ", (selectedUser === null || selectedUser === void 0 ? void 0 : selectedUser.name) || (selectedUser === null || selectedUser === void 0 ? void 0 : selectedUser.email_address), "? They will not be able to log in until reactivated."] })] }), _jsxs(AlertDialogFooter, { className: "cls_user_management_deactivate_dialog_footer", children: [_jsx(AlertDialogAction, { onClick: handleDeactivateUser, disabled: usersActionLoading, className: "cls_user_management_deactivate_dialog_confirm", children: usersActionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Deactivating..."] })) : ("Deactivate") }), _jsx(AlertDialogCancel, { onClick: () => {
|
|
470
|
+
}, variant: "outline", size: "sm", className: "cls_user_management_permissions_table_action_edit", children: [_jsx(Edit, { className: "h-4 w-4 mr-1" }), "Edit"] }), _jsxs(Button, { onClick: () => handleDeletePermission(permission), disabled: permissionsActionLoading, variant: "outline", size: "sm", className: "cls_user_management_permissions_table_action_delete text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-1" }), "Delete"] })] })) }) })] }, `${permission.source}-${permission.id}-${permission.permission_name}`)))) })] }) }))] }) })), showScopeLabelsTab && (_jsx(TabsContent, { value: "scope_labels", className: "cls_user_management_tab_scope_labels w-full", children: _jsx(ScopeLabelsTab, { defaultOrg: defaultOrg }) })), showScopeHierarchyTab && (_jsx(TabsContent, { value: "scope_hierarchy", className: "cls_user_management_tab_scope_hierarchy w-full", children: _jsx(ScopeHierarchyTab, { defaultOrg: defaultOrg }) })), showUserScopesTab && (_jsx(TabsContent, { value: "user_scopes", className: "cls_user_management_tab_user_scopes w-full", children: _jsx(UserScopesTab, {}) })), showOrgsTab && (_jsx(TabsContent, { value: "organizations", className: "cls_user_management_tab_organizations w-full", children: _jsx(OrgHierarchyTab, { isGlobalAdmin: hasOrgGlobalAdminPermission }) }))] })), _jsx(AlertDialog, { open: deactivateDialogOpen, onOpenChange: setDeactivateDialogOpen, children: _jsxs(AlertDialogContent, { className: "cls_user_management_deactivate_dialog", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Deactivate User" }), _jsxs(AlertDialogDescription, { children: ["Are you sure you want to deactivate ", (selectedUser === null || selectedUser === void 0 ? void 0 : selectedUser.name) || (selectedUser === null || selectedUser === void 0 ? void 0 : selectedUser.email_address), "? They will not be able to log in until reactivated."] })] }), _jsxs(AlertDialogFooter, { className: "cls_user_management_deactivate_dialog_footer", children: [_jsx(AlertDialogAction, { onClick: handleDeactivateUser, disabled: usersActionLoading, className: "cls_user_management_deactivate_dialog_confirm", children: usersActionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Deactivating..."] })) : ("Deactivate") }), _jsx(AlertDialogCancel, { onClick: () => {
|
|
465
471
|
setDeactivateDialogOpen(false);
|
|
466
472
|
setSelectedUser(null);
|
|
467
473
|
}, className: "cls_user_management_deactivate_dialog_cancel", children: "Cancel" })] })] }) }), _jsx(AlertDialog, { open: resetPasswordDialogOpen, onOpenChange: setResetPasswordDialogOpen, children: _jsxs(AlertDialogContent, { className: "cls_user_management_reset_password_dialog", children: [_jsxs(AlertDialogHeader, { children: [_jsx(AlertDialogTitle, { children: "Reset Password" }), _jsxs(AlertDialogDescription, { children: ["Send a password reset email to ", selectedUser === null || selectedUser === void 0 ? void 0 : selectedUser.email_address, "? They will receive a link to reset their password."] })] }), _jsxs(AlertDialogFooter, { className: "cls_user_management_reset_password_dialog_footer", children: [_jsx(AlertDialogAction, { onClick: handleResetPassword, disabled: usersActionLoading, className: "cls_user_management_reset_password_dialog_confirm", children: usersActionLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Sending..."] })) : ("Send Reset Email") }), _jsx(AlertDialogCancel, { onClick: () => {
|
|
@@ -7,6 +7,12 @@ export type HazoAuthUser = {
|
|
|
7
7
|
email_address: string;
|
|
8
8
|
is_active: boolean;
|
|
9
9
|
profile_picture_url: string | null;
|
|
10
|
+
org_id?: string | null;
|
|
11
|
+
org_name?: string | null;
|
|
12
|
+
parent_org_id?: string | null;
|
|
13
|
+
parent_org_name?: string | null;
|
|
14
|
+
root_org_id?: string | null;
|
|
15
|
+
root_org_name?: string | null;
|
|
10
16
|
};
|
|
11
17
|
/**
|
|
12
18
|
* Scope access information returned when HRBAC scope checking is used
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth_types.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"auth_types.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/auth_types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IAEnC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,cAAc,GACtB;IACE,aAAa,EAAE,IAAI,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,CAAC,EAAE,eAAe,CAAC;CACpC,GACD;IACE,aAAa,EAAE,KAAK,CAAC;IACrB,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,EAAE,CAAC;IAChB,aAAa,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC;AAEN;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;IAChC;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IAE/B,mBAAmB,EAAE,MAAM,EAAE;IAC7B,gBAAgB,EAAE,MAAM,EAAE;IAC1B,oBAAoB,EAAE,MAAM,EAAE;IAC9B,qBAAqB,CAAC,EAAE,MAAM;gBAH9B,mBAAmB,EAAE,MAAM,EAAE,EAC7B,gBAAgB,EAAE,MAAM,EAAE,EAC1B,oBAAoB,EAAE,MAAM,EAAE,EAC9B,qBAAqB,CAAC,EAAE,MAAM,YAAA;CAKxC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IAEhC,UAAU,EAAE,MAAM;IAClB,gBAAgB,EAAE,MAAM;IACxB,WAAW,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;gBAF/E,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,MAAM,EACxB,WAAW,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAKzF"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { NextRequest } from "next/server";
|
|
2
|
+
export type DevLockValidationResult = {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
expired?: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type DevLockCookieData = {
|
|
7
|
+
value: string;
|
|
8
|
+
max_age: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Creates a signed dev lock cookie value
|
|
12
|
+
* Cookie format: timestamp|expiry_timestamp|signature
|
|
13
|
+
* @param password - The dev lock password (used as signing key)
|
|
14
|
+
* @param expiry_days - Number of days until cookie expires (default: 7)
|
|
15
|
+
* @returns Cookie value and max_age in seconds
|
|
16
|
+
*/
|
|
17
|
+
export declare function create_dev_lock_cookie(password: string, expiry_days?: number): Promise<DevLockCookieData>;
|
|
18
|
+
/**
|
|
19
|
+
* Validates dev lock cookie from request (Edge-compatible)
|
|
20
|
+
* Checks signature validity and expiration
|
|
21
|
+
* @param request - NextRequest object
|
|
22
|
+
* @returns Validation result with valid flag and optional expired flag
|
|
23
|
+
*/
|
|
24
|
+
export declare function validate_dev_lock_cookie(request: NextRequest): Promise<DevLockValidationResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Validates password against environment variable (for unlock endpoint)
|
|
27
|
+
* Uses constant-time comparison to prevent timing attacks
|
|
28
|
+
* @param password - Password to validate
|
|
29
|
+
* @returns true if password matches
|
|
30
|
+
*/
|
|
31
|
+
export declare function validate_dev_lock_password(password: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Gets the dev lock cookie name
|
|
34
|
+
* Exported for use in API routes when setting the cookie
|
|
35
|
+
* @returns Cookie name string
|
|
36
|
+
*/
|
|
37
|
+
export declare function get_dev_lock_cookie_name(): string;
|
|
38
|
+
//# sourceMappingURL=dev_lock_validator.edge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev_lock_validator.edge.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/dev_lock_validator.edge.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAmDF;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,MAAM,EAChB,WAAW,GAAE,MAAU,GACtB,OAAO,CAAC,iBAAiB,CAAC,CAU5B;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CA8ClC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQpE;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// section: constants
|
|
2
|
+
const COOKIE_NAME = "hazo_auth_dev_lock";
|
|
3
|
+
const SEPARATOR = "|";
|
|
4
|
+
// section: helpers
|
|
5
|
+
/**
|
|
6
|
+
* Creates HMAC-SHA256 signature using Web Crypto API (Edge compatible)
|
|
7
|
+
* @param data - Data to sign
|
|
8
|
+
* @param secret - Secret key for signing
|
|
9
|
+
* @returns Hex string signature
|
|
10
|
+
*/
|
|
11
|
+
async function create_signature(data, secret) {
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const key_data = encoder.encode(secret);
|
|
14
|
+
const message_data = encoder.encode(data);
|
|
15
|
+
const crypto_key = await crypto.subtle.importKey("raw", key_data, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
16
|
+
const signature = await crypto.subtle.sign("HMAC", crypto_key, message_data);
|
|
17
|
+
// Convert ArrayBuffer to hex string
|
|
18
|
+
return Array.from(new Uint8Array(signature))
|
|
19
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
20
|
+
.join("");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Performs constant-time comparison of two strings
|
|
24
|
+
* Prevents timing attacks
|
|
25
|
+
* @param a - First string
|
|
26
|
+
* @param b - Second string
|
|
27
|
+
* @returns true if strings are equal
|
|
28
|
+
*/
|
|
29
|
+
function constant_time_compare(a, b) {
|
|
30
|
+
if (a.length !== b.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
let result = 0;
|
|
34
|
+
for (let i = 0; i < a.length; i++) {
|
|
35
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
36
|
+
}
|
|
37
|
+
return result === 0;
|
|
38
|
+
}
|
|
39
|
+
// section: main_functions
|
|
40
|
+
/**
|
|
41
|
+
* Creates a signed dev lock cookie value
|
|
42
|
+
* Cookie format: timestamp|expiry_timestamp|signature
|
|
43
|
+
* @param password - The dev lock password (used as signing key)
|
|
44
|
+
* @param expiry_days - Number of days until cookie expires (default: 7)
|
|
45
|
+
* @returns Cookie value and max_age in seconds
|
|
46
|
+
*/
|
|
47
|
+
export async function create_dev_lock_cookie(password, expiry_days = 7) {
|
|
48
|
+
const timestamp = Date.now();
|
|
49
|
+
const expiry_timestamp = timestamp + expiry_days * 24 * 60 * 60 * 1000;
|
|
50
|
+
const data = `${timestamp}${SEPARATOR}${expiry_timestamp}`;
|
|
51
|
+
const signature = await create_signature(data, password);
|
|
52
|
+
return {
|
|
53
|
+
value: `${data}${SEPARATOR}${signature}`,
|
|
54
|
+
max_age: expiry_days * 24 * 60 * 60, // in seconds
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Validates dev lock cookie from request (Edge-compatible)
|
|
59
|
+
* Checks signature validity and expiration
|
|
60
|
+
* @param request - NextRequest object
|
|
61
|
+
* @returns Validation result with valid flag and optional expired flag
|
|
62
|
+
*/
|
|
63
|
+
export async function validate_dev_lock_cookie(request) {
|
|
64
|
+
var _a;
|
|
65
|
+
const password = process.env.HAZO_AUTH_DEV_LOCK_PASSWORD;
|
|
66
|
+
if (!password) {
|
|
67
|
+
// No password set - cannot validate
|
|
68
|
+
return { valid: false };
|
|
69
|
+
}
|
|
70
|
+
const cookie = (_a = request.cookies.get(COOKIE_NAME)) === null || _a === void 0 ? void 0 : _a.value;
|
|
71
|
+
if (!cookie) {
|
|
72
|
+
return { valid: false };
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const parts = cookie.split(SEPARATOR);
|
|
76
|
+
if (parts.length !== 3) {
|
|
77
|
+
return { valid: false };
|
|
78
|
+
}
|
|
79
|
+
const [timestamp_str, expiry_str, signature] = parts;
|
|
80
|
+
const timestamp = parseInt(timestamp_str, 10);
|
|
81
|
+
const expiry_timestamp = parseInt(expiry_str, 10);
|
|
82
|
+
if (isNaN(timestamp) || isNaN(expiry_timestamp)) {
|
|
83
|
+
return { valid: false };
|
|
84
|
+
}
|
|
85
|
+
// Check expiry
|
|
86
|
+
if (Date.now() > expiry_timestamp) {
|
|
87
|
+
return { valid: false, expired: true };
|
|
88
|
+
}
|
|
89
|
+
// Verify signature
|
|
90
|
+
const data = `${timestamp}${SEPARATOR}${expiry_timestamp}`;
|
|
91
|
+
const expected_signature = await create_signature(data, password);
|
|
92
|
+
// Constant-time comparison to prevent timing attacks
|
|
93
|
+
if (!constant_time_compare(signature, expected_signature)) {
|
|
94
|
+
return { valid: false };
|
|
95
|
+
}
|
|
96
|
+
return { valid: true };
|
|
97
|
+
}
|
|
98
|
+
catch (_b) {
|
|
99
|
+
return { valid: false };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Validates password against environment variable (for unlock endpoint)
|
|
104
|
+
* Uses constant-time comparison to prevent timing attacks
|
|
105
|
+
* @param password - Password to validate
|
|
106
|
+
* @returns true if password matches
|
|
107
|
+
*/
|
|
108
|
+
export function validate_dev_lock_password(password) {
|
|
109
|
+
const expected = process.env.HAZO_AUTH_DEV_LOCK_PASSWORD;
|
|
110
|
+
if (!expected || !password) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
return constant_time_compare(password, expected);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Gets the dev lock cookie name
|
|
117
|
+
* Exported for use in API routes when setting the cookie
|
|
118
|
+
* @returns Cookie name string
|
|
119
|
+
*/
|
|
120
|
+
export function get_dev_lock_cookie_name() {
|
|
121
|
+
return COOKIE_NAME;
|
|
122
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hazo_get_auth.server.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/hazo_get_auth.server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EAAE,cAAc,EAAgB,eAAe,EAAmB,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"hazo_get_auth.server.d.ts","sourceRoot":"","sources":["../../../src/lib/auth/hazo_get_auth.server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,OAAO,KAAK,EAAE,cAAc,EAAgB,eAAe,EAAmB,MAAM,cAAc,CAAC;AAiWnG;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAgNzB"}
|