Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Update frontend/src/pages/SuperAdminPanel.jsx
Browse files
frontend/src/pages/SuperAdminPanel.jsx
CHANGED
|
@@ -504,6 +504,7 @@ const SuperAdminPanel = () => {
|
|
| 504 |
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
|
| 505 |
const [promptModal, setPromptModal] = useState({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null });
|
| 506 |
const [promptValues, setPromptValues] = useState({});
|
|
|
|
| 507 |
const [viewInvoice, setViewInvoice] = useState(null);
|
| 508 |
const [rescheduleModal, setRescheduleModal] = useState({
|
| 509 |
isOpen: false,
|
|
@@ -517,7 +518,7 @@ const SuperAdminPanel = () => {
|
|
| 517 |
});
|
| 518 |
|
| 519 |
const closeConfirm = () => setConfirmModal({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
|
| 520 |
-
const closePrompt = () => { setPromptModal({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null }); setPromptValues({}); };
|
| 521 |
|
| 522 |
const handlePromptChange = (key, value) => setPromptValues(prev => ({ ...prev, [key]: value }));
|
| 523 |
|
|
@@ -845,7 +846,7 @@ const SuperAdminPanel = () => {
|
|
| 845 |
desc: 'Invite or add a new global platform member.',
|
| 846 |
inputs: [
|
| 847 |
{ key: 'email', label: 'User Email', placeholder: 'user@example.com' },
|
| 848 |
-
{ key: 'password', label: 'Password (Optional - Auto-generated if left blank)', placeholder: 'Set initial password...', type: 'password' },
|
| 849 |
{ key: 'role', label: 'Role', type: 'select', options: MEMBER_ROLE_OPTIONS },
|
| 850 |
{ key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
|
| 851 |
],
|
|
@@ -854,6 +855,11 @@ const SuperAdminPanel = () => {
|
|
| 854 |
toast.error('Please enter a valid user email');
|
| 855 |
return;
|
| 856 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 857 |
try {
|
| 858 |
const res = await authFetch('/api/auth/users', {
|
| 859 |
method: 'POST',
|
|
@@ -862,7 +868,7 @@ const SuperAdminPanel = () => {
|
|
| 862 |
email: values.email.trim(),
|
| 863 |
role: values.role,
|
| 864 |
org_id: values.org_id || null,
|
| 865 |
-
password:
|
| 866 |
})
|
| 867 |
});
|
| 868 |
const data = await res.json().catch(() => ({}));
|
|
@@ -905,11 +911,16 @@ const SuperAdminPanel = () => {
|
|
| 905 |
desc: `Update details & credentials for ${u.email}`,
|
| 906 |
inputs: [
|
| 907 |
{ key: 'email', label: 'User Email', disabled: true },
|
| 908 |
-
{ key: 'password', label: 'New Password (Leave blank to keep current password)', placeholder: 'Type new password to update...', type: 'password' },
|
| 909 |
{ key: 'role', label: 'Role', type: 'select', options: MEMBER_ROLE_OPTIONS },
|
| 910 |
{ key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
|
| 911 |
],
|
| 912 |
onConfirm: async (vals) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 913 |
try {
|
| 914 |
const res = await authFetch(`/api/auth/users/${u.id}`, {
|
| 915 |
method: 'PUT',
|
|
@@ -917,7 +928,7 @@ const SuperAdminPanel = () => {
|
|
| 917 |
body: JSON.stringify({
|
| 918 |
role: vals.role,
|
| 919 |
org_id: vals.org_id || null,
|
| 920 |
-
password:
|
| 921 |
})
|
| 922 |
});
|
| 923 |
const data = await res.json().catch(() => ({}));
|
|
@@ -1893,6 +1904,57 @@ const SuperAdminPanel = () => {
|
|
| 1893 |
return <option key={val} value={val}>{lbl}</option>;
|
| 1894 |
})}
|
| 1895 |
</select>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1896 |
) : (
|
| 1897 |
<input
|
| 1898 |
type={input.type || "text"}
|
|
|
|
| 504 |
const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
|
| 505 |
const [promptModal, setPromptModal] = useState({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null });
|
| 506 |
const [promptValues, setPromptValues] = useState({});
|
| 507 |
+
const [showPasswordPrompt, setShowPasswordPrompt] = useState({});
|
| 508 |
const [viewInvoice, setViewInvoice] = useState(null);
|
| 509 |
const [rescheduleModal, setRescheduleModal] = useState({
|
| 510 |
isOpen: false,
|
|
|
|
| 518 |
});
|
| 519 |
|
| 520 |
const closeConfirm = () => setConfirmModal({ isOpen: false, title: '', desc: '', onConfirm: null, type: 'primary' });
|
| 521 |
+
const closePrompt = () => { setPromptModal({ isOpen: false, title: '', desc: '', inputs: [], onConfirm: null }); setPromptValues({}); setShowPasswordPrompt({}); };
|
| 522 |
|
| 523 |
const handlePromptChange = (key, value) => setPromptValues(prev => ({ ...prev, [key]: value }));
|
| 524 |
|
|
|
|
| 846 |
desc: 'Invite or add a new global platform member.',
|
| 847 |
inputs: [
|
| 848 |
{ key: 'email', label: 'User Email', placeholder: 'user@example.com' },
|
| 849 |
+
{ key: 'password', label: 'Password (Optional - Auto-generated if left blank)', placeholder: 'Set initial password...', type: 'password', showRules: true },
|
| 850 |
{ key: 'role', label: 'Role', type: 'select', options: MEMBER_ROLE_OPTIONS },
|
| 851 |
{ key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
|
| 852 |
],
|
|
|
|
| 855 |
toast.error('Please enter a valid user email');
|
| 856 |
return;
|
| 857 |
}
|
| 858 |
+
const pwd = values.password ? values.password.trim() : '';
|
| 859 |
+
if (pwd && (pwd.length < 8 || !/[A-Z]/.test(pwd) || !/[a-z]/.test(pwd) || !/[^A-Za-z0-9]/.test(pwd))) {
|
| 860 |
+
toast.error('Password must meet all 4 security requirements (8+ chars, 1 uppercase, 1 lowercase, 1 special character).');
|
| 861 |
+
return;
|
| 862 |
+
}
|
| 863 |
try {
|
| 864 |
const res = await authFetch('/api/auth/users', {
|
| 865 |
method: 'POST',
|
|
|
|
| 868 |
email: values.email.trim(),
|
| 869 |
role: values.role,
|
| 870 |
org_id: values.org_id || null,
|
| 871 |
+
password: pwd || null
|
| 872 |
})
|
| 873 |
});
|
| 874 |
const data = await res.json().catch(() => ({}));
|
|
|
|
| 911 |
desc: `Update details & credentials for ${u.email}`,
|
| 912 |
inputs: [
|
| 913 |
{ key: 'email', label: 'User Email', disabled: true },
|
| 914 |
+
{ key: 'password', label: 'New Password (Leave blank to keep current password)', placeholder: 'Type new password to update...', type: 'password', showRules: true },
|
| 915 |
{ key: 'role', label: 'Role', type: 'select', options: MEMBER_ROLE_OPTIONS },
|
| 916 |
{ key: 'org_id', label: 'Organization', type: 'select', options: orgOptions }
|
| 917 |
],
|
| 918 |
onConfirm: async (vals) => {
|
| 919 |
+
const pwd = vals.password ? vals.password.trim() : '';
|
| 920 |
+
if (pwd && (pwd.length < 8 || !/[A-Z]/.test(pwd) || !/[a-z]/.test(pwd) || !/[^A-Za-z0-9]/.test(pwd))) {
|
| 921 |
+
toast.error('Password must meet all 4 security requirements (8+ chars, 1 uppercase, 1 lowercase, 1 special character).');
|
| 922 |
+
return;
|
| 923 |
+
}
|
| 924 |
try {
|
| 925 |
const res = await authFetch(`/api/auth/users/${u.id}`, {
|
| 926 |
method: 'PUT',
|
|
|
|
| 928 |
body: JSON.stringify({
|
| 929 |
role: vals.role,
|
| 930 |
org_id: vals.org_id || null,
|
| 931 |
+
password: pwd || null
|
| 932 |
})
|
| 933 |
});
|
| 934 |
const data = await res.json().catch(() => ({}));
|
|
|
|
| 1904 |
return <option key={val} value={val}>{lbl}</option>;
|
| 1905 |
})}
|
| 1906 |
</select>
|
| 1907 |
+
) : input.type === 'password' ? (
|
| 1908 |
+
<div>
|
| 1909 |
+
<div className="relative">
|
| 1910 |
+
<input
|
| 1911 |
+
type={showPasswordPrompt[input.key] ? "text" : "password"}
|
| 1912 |
+
value={promptValues[input.key] || ''}
|
| 1913 |
+
onChange={(e) => handlePromptChange(input.key, e.target.value)}
|
| 1914 |
+
placeholder={input.placeholder}
|
| 1915 |
+
disabled={input.disabled}
|
| 1916 |
+
className="w-full bg-surface-container border border-outline-variant rounded-lg pl-3 pr-10 py-2 focus:border-primary outline-none text-on-surface text-sm"
|
| 1917 |
+
/>
|
| 1918 |
+
<button
|
| 1919 |
+
type="button"
|
| 1920 |
+
onClick={() => setShowPasswordPrompt(prev => ({ ...prev, [input.key]: !prev[input.key] }))}
|
| 1921 |
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-on-surface-variant hover:text-primary bg-transparent border-0 cursor-pointer p-1 flex items-center justify-center"
|
| 1922 |
+
title={showPasswordPrompt[input.key] ? "Hide password" : "Show password"}
|
| 1923 |
+
>
|
| 1924 |
+
<span className="material-symbols-outlined text-[18px]">
|
| 1925 |
+
{showPasswordPrompt[input.key] ? 'visibility_off' : 'visibility'}
|
| 1926 |
+
</span>
|
| 1927 |
+
</button>
|
| 1928 |
+
</div>
|
| 1929 |
+
{input.showRules && (promptValues[input.key] || '').length > 0 && (
|
| 1930 |
+
<div className="flex flex-col gap-1.5 p-3 bg-surface-container-high/60 border border-outline-variant/60 rounded-xl text-xs mt-2 animate-fade-in">
|
| 1931 |
+
<div className={`flex items-center gap-2 font-medium transition-colors ${(promptValues[input.key] || '').length >= 8 ? 'text-green-600 dark:text-green-500 font-bold' : 'text-on-surface-variant'}`}>
|
| 1932 |
+
<span className="material-symbols-outlined text-[16px]">
|
| 1933 |
+
{(promptValues[input.key] || '').length >= 8 ? 'check_circle' : 'radio_button_unchecked'}
|
| 1934 |
+
</span>
|
| 1935 |
+
<span>At least 8 characters</span>
|
| 1936 |
+
</div>
|
| 1937 |
+
<div className={`flex items-center gap-2 font-medium transition-colors ${/[A-Z]/.test(promptValues[input.key] || '') ? 'text-green-600 dark:text-green-500 font-bold' : 'text-on-surface-variant'}`}>
|
| 1938 |
+
<span className="material-symbols-outlined text-[16px]">
|
| 1939 |
+
{/[A-Z]/.test(promptValues[input.key] || '') ? 'check_circle' : 'radio_button_unchecked'}
|
| 1940 |
+
</span>
|
| 1941 |
+
<span>One uppercase letter</span>
|
| 1942 |
+
</div>
|
| 1943 |
+
<div className={`flex items-center gap-2 font-medium transition-colors ${/[a-z]/.test(promptValues[input.key] || '') ? 'text-green-600 dark:text-green-500 font-bold' : 'text-on-surface-variant'}`}>
|
| 1944 |
+
<span className="material-symbols-outlined text-[16px]">
|
| 1945 |
+
{/[a-z]/.test(promptValues[input.key] || '') ? 'check_circle' : 'radio_button_unchecked'}
|
| 1946 |
+
</span>
|
| 1947 |
+
<span>One lowercase letter</span>
|
| 1948 |
+
</div>
|
| 1949 |
+
<div className={`flex items-center gap-2 font-medium transition-colors ${/[^A-Za-z0-9]/.test(promptValues[input.key] || '') ? 'text-green-600 dark:text-green-500 font-bold' : 'text-on-surface-variant'}`}>
|
| 1950 |
+
<span className="material-symbols-outlined text-[16px]">
|
| 1951 |
+
{/[^A-Za-z0-9]/.test(promptValues[input.key] || '') ? 'check_circle' : 'radio_button_unchecked'}
|
| 1952 |
+
</span>
|
| 1953 |
+
<span>One special character</span>
|
| 1954 |
+
</div>
|
| 1955 |
+
</div>
|
| 1956 |
+
)}
|
| 1957 |
+
</div>
|
| 1958 |
) : (
|
| 1959 |
<input
|
| 1960 |
type={input.type || "text"}
|