// Admin page — pending access requests + user management // Only rendered if window.APP_USER.is_admin is true. function AdminPage() { const u = window.APP_USER || {}; if (!u.is_admin) { return (
); } const [tab, setTab] = useState('requests'); // requests | users const [requests, setRequests] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); const [editingUser, setEditingUser] = useState(null); const reload = async () => { setLoading(true); try { const [r1, r2] = await Promise.all([ apiFetch('/api/admin/access_requests?status=pending').then(r => r.json()), apiFetch('/api/admin/users').then(r => r.json()), ]); setRequests(r1); setUsers(r2); } catch (err) { console.error(err); } finally { setLoading(false); } }; useEffect(() => { reload(); }, []); const pendingCount = requests.filter(r => r.status === 'pending').length; return (

ניהול

ניהול משתמשים ובקשות גישה
{/* Tabs */}
setTab('requests')} label={`בקשות גישה`} count={pendingCount} /> setTab('users')} label="משתמשים" count={users.length} />
{loading ? ( ) : tab === 'requests' ? ( ) : ( )} {showCreate && setShowCreate(false)} onCreated={() => { setShowCreate(false); reload(); }} />} {editingUser && setEditingUser(null)} onSaved={() => { setEditingUser(null); reload(); }} currentUserId={u.id} />}
); } function AdminTab({ on, onClick, label, count }) { return ( ); } function RequestsList({ requests, onAction }) { const [busy, setBusy] = useState(null); const [tempPasswords, setTempPasswords] = useState({}); // { request_id: password } const approve = async (r) => { if (!confirm(`לאשר גישה ל-${r.email}? סיסמה זמנית תיווצר ותוצג לך פעם אחת.`)) return; setBusy(r.id); try { const resp = await apiFetch(`/api/admin/access_requests/${r.id}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); if (!resp.ok) throw new Error((await resp.json()).detail || 'אישור נכשל'); const data = await resp.json(); setTempPasswords(prev => ({ ...prev, [r.id]: data.temporary_password })); // Don't immediately refresh — keep showing the temp password until user dismisses } catch (err) { alert('שגיאה: ' + err.message); setBusy(null); } }; const deny = async (r) => { if (!confirm(`לדחות את הבקשה של ${r.email}?`)) return; setBusy(r.id); try { const resp = await apiFetch(`/api/admin/access_requests/${r.id}/deny`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); if (!resp.ok) throw new Error('דחייה נכשלה'); onAction(); } catch (err) { alert('שגיאה: ' + err.message); setBusy(null); } }; if (requests.length === 0) { return ; } return ( {requests.map((r, i) => { const tempPw = tempPasswords[r.id]; return (
{(r.name || r.email)[0].toUpperCase()}
{r.name}
{r.email}
{r.reason && (
{r.reason}
)}
{new Date(r.created_at).toLocaleString('he-IL')} · {r.ip_address || '—'}
{!tempPw && (
)}
{tempPw && (
✓ המשתמש נוצר. שתף את הסיסמה הזמנית עם {r.name}:
{tempPw}
הסיסמה תוצג רק פעם אחת. שלח אותה למשתמש ב-WhatsApp/SMS — הוא יחליף אותה אחרי הכניסה הראשונה.
)}
); })}
); } function UsersList({ users, onEdit, onAction, currentUserId }) { return ( {users.map((u, i) => (
{(u.name || u.email)[0].toUpperCase()}
{u.name} {u.is_admin && ( אדמין )} {!u.is_active && ( לא פעיל )} {u.id === currentUserId && ( (אתה) )}
{u.email} · {u.active_sessions} סשנים פעילים {u.last_login_at && ` · נכנס ${new Date(u.last_login_at).toLocaleDateString('he-IL')}`}
))}
); } function CreateUserModal({ onClose, onCreated }) { const [email, setEmail] = useState(''); const [name, setName] = useState(''); const [password, setPassword] = useState(''); const [isAdmin, setIsAdmin] = useState(false); const [saving, setSaving] = useState(false); const [tempPassword, setTempPassword] = useState(null); const [createdEmail, setCreatedEmail] = useState(null); const submit = async (e) => { e.preventDefault(); setSaving(true); try { const r = await apiFetch('/api/admin/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email.trim(), name: name.trim(), password: password.trim() || null, is_admin: isAdmin, }), }); if (!r.ok) { const err = await r.json().catch(() => ({})); const msg = (typeof err.detail === 'string') ? err.detail : (err.detail?.message || JSON.stringify(err.detail) || 'יצירה נכשלה'); throw new Error(msg); } const data = await r.json(); setTempPassword(data.temporary_password); setCreatedEmail(data.user.email); } catch (err) { alert('שגיאה: ' + err.message); } finally { setSaving(false); } }; return (
e.stopPropagation()} onSubmit={submit} className="slide-up" style={{ width: 460, maxWidth: '90%', background: 'var(--surface)', borderRadius: 16, boxShadow: 'var(--shadow-lg)', overflow: 'hidden', }}>
יצירת משתמש חדש
{!tempPassword ? ( <>
setEmail(e.target.value)} type="email" required style={inpStyle} /> setName(e.target.value)} required style={inpStyle} /> setPassword(e.target.value)} type="text" minLength={10} style={inpStyle} />
) : ( <>
✓ המשתמש נוצר
אימייל: {createdEmail}
שתף את הסיסמה הזמנית — היא תוצג רק פעם אחת:
{tempPassword}
)}
); } function EditUserModal({ user, onClose, onSaved, currentUserId }) { const [name, setName] = useState(user.name); const [isAdmin, setIsAdmin] = useState(user.is_admin); const [isActive, setIsActive] = useState(user.is_active); const [newPw, setNewPw] = useState(''); const [saving, setSaving] = useState(false); const isSelf = user.id === currentUserId; const save = async (e) => { e.preventDefault(); setSaving(true); try { const r = await apiFetch(`/api/admin/users/${user.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name.trim() !== user.name ? name.trim() : null, is_admin: isAdmin !== user.is_admin ? isAdmin : null, is_active: isActive !== user.is_active ? isActive : null, }), }); if (!r.ok) throw new Error((await r.json()).detail || 'שמירה נכשלה'); onSaved(); } catch (err) { alert('שגיאה: ' + err.message); setSaving(false); } }; const resetPassword = async () => { if (!newPw || newPw.length < 10) { alert('סיסמה חייבת לפחות 10 תווים'); return; } if (!confirm(`לאפס סיסמה ל-${user.email}? כל הסשנים שלו יופסקו.`)) return; setSaving(true); try { const r = await apiFetch(`/api/admin/users/${user.id}/set_password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: newPw }), }); if (!r.ok) { // Surface the real server error: it's usually // "סיסמה לא יכולה להכיל את האימייל" or a similar strength rule. const err = await r.json().catch(() => ({})); const msg = (typeof err.detail === 'string') ? err.detail : (err.detail?.message || JSON.stringify(err.detail) || 'איפוס נכשל'); throw new Error(msg); } const data = await r.json().catch(() => ({})); alert(`הסיסמה אופסה בהצלחה.${data.sessions_revoked ? ` ${data.sessions_revoked} סשנים פעילים נותקו.` : ''}\nשלח את הסיסמה החדשה למשתמש בדיוק כמו שהזנת אותה (case-sensitive, ללא רווחים).`); setNewPw(''); } catch (err) { alert('שגיאה: ' + err.message); } finally { setSaving(false); } }; const revokeSessions = async () => { if (!confirm(`לנתק את כל הסשנים של ${user.email}?`)) return; setSaving(true); try { await apiFetch(`/api/admin/users/${user.id}/revoke_sessions`, { method: 'POST', }); onSaved(); } catch (err) { alert('שגיאה: ' + err.message); setSaving(false); } }; return (
e.stopPropagation()} className="slide-up" style={{ width: 480, maxWidth: '90%', background: 'var(--surface)', borderRadius: 16, boxShadow: 'var(--shadow-lg)', overflow: 'hidden', maxHeight: '85vh', display: 'flex', flexDirection: 'column', }}>
עריכת {user.name}
{user.email} · נוצר {user.created_at ? new Date(user.created_at).toLocaleDateString('he-IL') : '—'}
setName(e.target.value)} required style={inpStyle} />
איפוס סיסמה
הגדר סיסמה חדשה. כל הסשנים של המשתמש יופסקו.
setNewPw(e.target.value)} type="text" placeholder="סיסמה חדשה (10+ תווים)" style={{ ...inpStyle, flex: 1 }} />
); } window.AdminPage = AdminPage;