@xeplr/ui-account 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +17 -0
- package/src/AccessContext.jsx +125 -0
- package/src/AccessGuard.jsx +35 -0
- package/src/ProtectedRoute.jsx +55 -0
- package/src/ThemeContext.jsx +84 -0
- package/src/adminApi.js +68 -0
- package/src/api.js +196 -0
- package/src/designs/AccessMatrixSample.jsx +199 -0
- package/src/designs/ActivateSample.jsx +38 -0
- package/src/designs/ChangePasswordSample.jsx +31 -0
- package/src/designs/ForgotPasswordSample.jsx +22 -0
- package/src/designs/LoginSample.jsx +26 -0
- package/src/designs/MasterSettingsSample.jsx +206 -0
- package/src/designs/NotActivatedSample.jsx +17 -0
- package/src/designs/ProfileSample.jsx +36 -0
- package/src/designs/RegisterSample.jsx +34 -0
- package/src/designs/ResetPasswordSample.jsx +42 -0
- package/src/designs/TenantPickerSample.jsx +39 -0
- package/src/designs/TenantSample.jsx +182 -0
- package/src/designs/UserRolesMatrixSample.jsx +77 -0
- package/src/designs/admin.css +525 -0
- package/src/designs/auth.css +160 -0
- package/src/designs/index.js +13 -0
- package/src/designs/theme.css +231 -0
- package/src/index.js +41 -0
- package/src/masterApi.js +28 -0
- package/src/pages.jsx +92 -0
- package/src/token.js +38 -0
- package/src/useAccessMatrixController.js +244 -0
- package/src/useActivateController.js +37 -0
- package/src/useChangePasswordController.js +48 -0
- package/src/useForgotPasswordController.js +32 -0
- package/src/useLoginController.js +53 -0
- package/src/useMasterSettingsController.js +199 -0
- package/src/useProfileController.js +62 -0
- package/src/useRegisterController.js +41 -0
- package/src/useResetPasswordController.js +36 -0
- package/src/useTenantController.js +146 -0
- package/src/useTenantPickerController.js +97 -0
- package/src/useUserRolesController.js +81 -0
- package/src/validateDesign.js +101 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { getMyTenants, logoutUser } from './api.js';
|
|
3
|
+
import { clearAuth } from './token.js';
|
|
4
|
+
|
|
5
|
+
var STORAGE_KEY = 'xeplr:activeTenant';
|
|
6
|
+
var COUNT_KEY = 'xeplr:tenantCount';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the currently selected tenant from localStorage.
|
|
10
|
+
*/
|
|
11
|
+
export function getActiveTenant() {
|
|
12
|
+
try {
|
|
13
|
+
var raw = localStorage.getItem(STORAGE_KEY);
|
|
14
|
+
return raw ? JSON.parse(raw) : null;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Set the active tenant in localStorage.
|
|
22
|
+
*/
|
|
23
|
+
export function setActiveTenant(tenant) {
|
|
24
|
+
if (tenant) {
|
|
25
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(tenant));
|
|
26
|
+
} else {
|
|
27
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear the active tenant (on logout).
|
|
33
|
+
*/
|
|
34
|
+
export function clearActiveTenant() {
|
|
35
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
36
|
+
localStorage.removeItem(COUNT_KEY);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if the user has multiple tenants (show switch option).
|
|
41
|
+
*/
|
|
42
|
+
export function hasMultipleTenants() {
|
|
43
|
+
try {
|
|
44
|
+
return parseInt(localStorage.getItem(COUNT_KEY) || '0') > 1;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Controller for the tenant picker screen shown after login.
|
|
52
|
+
*/
|
|
53
|
+
export function useTenantPickerController(options = {}) {
|
|
54
|
+
var [tenants, setTenants] = useState([]);
|
|
55
|
+
var [loading, setLoading] = useState(true);
|
|
56
|
+
var [error, setError] = useState('');
|
|
57
|
+
var level = options.level || 1;
|
|
58
|
+
var label = options.label || 'Company';
|
|
59
|
+
|
|
60
|
+
useEffect(function() {
|
|
61
|
+
setLoading(true);
|
|
62
|
+
getMyTenants()
|
|
63
|
+
.then(function(data) {
|
|
64
|
+
setTenants(data);
|
|
65
|
+
try { localStorage.setItem(COUNT_KEY, String(data.length)); } catch(e) {}
|
|
66
|
+
// Auto-select if only one tenant
|
|
67
|
+
if (data.length === 1) {
|
|
68
|
+
setActiveTenant(data[0]);
|
|
69
|
+
if (options.onSelect) options.onSelect(data[0]);
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.catch(function(err) { setError(err.message); })
|
|
73
|
+
.finally(function() { setLoading(false); });
|
|
74
|
+
}, [level]);
|
|
75
|
+
|
|
76
|
+
function handleSelect(tenant) {
|
|
77
|
+
setActiveTenant(tenant);
|
|
78
|
+
if (options.onSelect) options.onSelect(tenant);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleLogout() {
|
|
82
|
+
clearActiveTenant();
|
|
83
|
+
logoutUser();
|
|
84
|
+
clearAuth();
|
|
85
|
+
if (options.onLogout) options.onLogout();
|
|
86
|
+
else window.location.href = '/login';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
tenants,
|
|
91
|
+
loading,
|
|
92
|
+
error,
|
|
93
|
+
label,
|
|
94
|
+
handleSelect,
|
|
95
|
+
onLogout: handleLogout
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { getUsers, getRoles, toggleUserRole } from './adminApi.js';
|
|
3
|
+
|
|
4
|
+
export function useUserRolesController() {
|
|
5
|
+
var [users, setUsers] = useState([]);
|
|
6
|
+
var [roles, setRoles] = useState([]);
|
|
7
|
+
var [search, setSearch] = useState('');
|
|
8
|
+
var [loading, setLoading] = useState(true);
|
|
9
|
+
var [error, setError] = useState('');
|
|
10
|
+
var [saving, setSaving] = useState({});
|
|
11
|
+
|
|
12
|
+
useEffect(function() {
|
|
13
|
+
loadData();
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
async function loadData() {
|
|
17
|
+
setLoading(true);
|
|
18
|
+
setError('');
|
|
19
|
+
try {
|
|
20
|
+
var [usersData, rolesData] = await Promise.all([getUsers(), getRoles()]);
|
|
21
|
+
setUsers(usersData);
|
|
22
|
+
setRoles(rolesData.filter(function(r) { return r.name !== 'Super Admin'; }));
|
|
23
|
+
} catch (err) {
|
|
24
|
+
setError(err.message);
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var filteredUsers = useMemo(function() {
|
|
31
|
+
if (!search.trim()) return users;
|
|
32
|
+
var q = search.toLowerCase();
|
|
33
|
+
return users.filter(function(u) {
|
|
34
|
+
return (u.name && u.name.toLowerCase().includes(q)) ||
|
|
35
|
+
(u.email && u.email.toLowerCase().includes(q));
|
|
36
|
+
});
|
|
37
|
+
}, [users, search]);
|
|
38
|
+
|
|
39
|
+
var handleToggle = useCallback(async function(userId, roleId, currentlyAssigned) {
|
|
40
|
+
var key = userId + ':' + roleId;
|
|
41
|
+
setSaving(function(prev) { var next = { ...prev }; next[key] = true; return next; });
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await toggleUserRole({ userId, roleId, assign: !currentlyAssigned });
|
|
45
|
+
setUsers(function(prev) {
|
|
46
|
+
return prev.map(function(u) {
|
|
47
|
+
if (u.id !== userId) return u;
|
|
48
|
+
var newRoles;
|
|
49
|
+
if (currentlyAssigned) {
|
|
50
|
+
newRoles = u.roles.filter(function(r) { return r.id !== roleId; });
|
|
51
|
+
} else {
|
|
52
|
+
var role = roles.find(function(r) { return r.id === roleId; });
|
|
53
|
+
newRoles = u.roles.concat(role ? [role] : []);
|
|
54
|
+
}
|
|
55
|
+
return { ...u, roles: newRoles };
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
setError(err.message);
|
|
60
|
+
} finally {
|
|
61
|
+
setSaving(function(prev) { var next = { ...prev }; delete next[key]; return next; });
|
|
62
|
+
}
|
|
63
|
+
}, [roles]);
|
|
64
|
+
|
|
65
|
+
function isAssigned(user, roleId) {
|
|
66
|
+
return user.roles && user.roles.some(function(r) { return r.id === roleId; });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
users: filteredUsers,
|
|
71
|
+
roles,
|
|
72
|
+
search,
|
|
73
|
+
setSearch,
|
|
74
|
+
loading,
|
|
75
|
+
error,
|
|
76
|
+
saving,
|
|
77
|
+
handleToggle,
|
|
78
|
+
isAssigned,
|
|
79
|
+
reload: loadData,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates that required elements exist in the rendered design.
|
|
5
|
+
* Throws a visible error if any required element is missing.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} componentName - Name of the page (for error messages)
|
|
8
|
+
* @param {Array<{id?: string, role?: string, selector?: string, label: string}>} requiredElements
|
|
9
|
+
*/
|
|
10
|
+
export function useDesignValidator(componentName, requiredElements) {
|
|
11
|
+
var containerRef = useRef(null);
|
|
12
|
+
|
|
13
|
+
useEffect(function() {
|
|
14
|
+
if (!containerRef.current) return;
|
|
15
|
+
|
|
16
|
+
// Delay validation to allow async data to render
|
|
17
|
+
var timer = setTimeout(function() {
|
|
18
|
+
if (!containerRef.current) return;
|
|
19
|
+
|
|
20
|
+
var missing = [];
|
|
21
|
+
for (var i = 0; i < requiredElements.length; i++) {
|
|
22
|
+
var rule = requiredElements[i];
|
|
23
|
+
var found = false;
|
|
24
|
+
|
|
25
|
+
if (rule.id) {
|
|
26
|
+
found = !!containerRef.current.querySelector('#' + rule.id);
|
|
27
|
+
} else if (rule.role) {
|
|
28
|
+
found = !!containerRef.current.querySelector('[role="' + rule.role + '"]');
|
|
29
|
+
} else if (rule.selector) {
|
|
30
|
+
found = !!containerRef.current.querySelector(rule.selector);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!found) {
|
|
34
|
+
missing.push(rule.label + (rule.id ? ' (id="' + rule.id + '")' : '') + (rule.selector ? ' (' + rule.selector + ')' : ''));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (missing.length > 0 && process.env.NODE_ENV !== 'production') {
|
|
39
|
+
console.warn(
|
|
40
|
+
'[xeplr-ui-account] ' + componentName + ' design is missing required elements:\n' +
|
|
41
|
+
missing.map(function(m) { return ' - ' + m; }).join('\n')
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}, 3000);
|
|
45
|
+
|
|
46
|
+
return function() { clearTimeout(timer); };
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return containerRef;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export var LOGIN_RULES = [
|
|
53
|
+
{ id: 'xeplr-email', label: 'Email input' },
|
|
54
|
+
{ id: 'xeplr-password', label: 'Password input' },
|
|
55
|
+
{ selector: 'button[type="submit"]', label: 'Submit button' }
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export var REGISTER_RULES = [
|
|
59
|
+
{ id: 'xeplr-name', label: 'Name input' },
|
|
60
|
+
{ id: 'xeplr-email', label: 'Email input' },
|
|
61
|
+
{ id: 'xeplr-password', label: 'Password input' },
|
|
62
|
+
{ selector: 'button[type="submit"]', label: 'Submit button' }
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export var FORGOT_PASSWORD_RULES = [
|
|
66
|
+
{ id: 'xeplr-email', label: 'Email input' },
|
|
67
|
+
{ selector: 'button[type="submit"]', label: 'Submit button' }
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
export var RESET_PASSWORD_RULES = [
|
|
71
|
+
{ id: 'xeplr-password', label: 'Password input' },
|
|
72
|
+
{ selector: 'button[type="submit"]', label: 'Submit button' }
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export var CHANGE_PASSWORD_RULES = [
|
|
76
|
+
{ id: 'xeplr-current-password', label: 'Current password input' },
|
|
77
|
+
{ id: 'xeplr-new-password', label: 'New password input' },
|
|
78
|
+
{ id: 'xeplr-confirm-password', label: 'Confirm password input' },
|
|
79
|
+
{ selector: 'button[type="submit"]', label: 'Submit button' }
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
export var PROFILE_RULES = [
|
|
83
|
+
{ id: 'xeplr-profile-name', label: 'Name input' },
|
|
84
|
+
{ id: 'xeplr-profile-email', label: 'Email input' },
|
|
85
|
+
{ selector: 'button[type="submit"]', label: 'Submit button' }
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
export var USER_ROLES_MATRIX_RULES = [
|
|
89
|
+
{ id: 'xeplr-admin-user-search', label: 'User search input' },
|
|
90
|
+
{ role: 'grid', label: 'Matrix grid table' }
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
export var ACCESS_MATRIX_RULES = [
|
|
94
|
+
{ id: 'xeplr-admin-access-search', label: 'Access search input' },
|
|
95
|
+
{ role: 'tablist', label: 'Tab list for Modules/Uncategorized' }
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
export var MASTER_SETTINGS_RULES = [
|
|
99
|
+
{ id: 'xeplr-admin-master-search', label: 'Master search input' },
|
|
100
|
+
{ role: 'tablist', label: 'Tab list for master types' }
|
|
101
|
+
];
|