Spaces:
Running
Running
| import { useState } from 'react'; | |
| import { req, saveToken, ENDPOINTS } from '../api'; | |
| import { Card, CardHeader, Callout, FormGroup, ResponseBox } from '../components/ui'; | |
| const { AUTH, ADMIN, RISK } = ENDPOINTS; | |
| function EmailStatusBody({ data }) { | |
| if (!data) return <p className="text-muted text-sm">Click Refresh to check email configuration.</p>; | |
| const configured = data.configured; | |
| const fields = data.fields || {}; | |
| return ( | |
| <div> | |
| <div className="flex items-center gap-2 mb-3"> | |
| <span className={`pill ${configured ? 'pill-ok' : 'pill-err'}`}> | |
| {configured ? 'β Configured' : 'β Not Configured'} | |
| </span> | |
| {data.mail_port && ( | |
| <span className="text-sm text-muted"> | |
| Port: {data.mail_port} Β· STARTTLS: {data.starttls ? 'Yes' : 'No'} | |
| </span> | |
| )} | |
| </div> | |
| {Object.entries(fields).map(([k, v]) => ( | |
| <div className="env-row" key={k}> | |
| <span className="env-key">ADAPTIVEAUTH_{k}</span> | |
| <span className={`pill ${v ? 'pill-ok' : 'pill-err'}`}>{v ? 'Set' : 'Missing'}</span> | |
| </div> | |
| ))} | |
| {!configured && data.setup_instructions && ( | |
| <Callout type="warn" style={{ marginTop: 12 }}> | |
| <strong>Setup:</strong> Create a <code>.env</code> file with the missing fields above.<br /> | |
| <code style={{ background: 'none', border: 'none', padding: 0, color: 'inherit', display: 'block', marginTop: 4 }}> | |
| {data.setup_instructions} | |
| </code> | |
| </Callout> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export default function AdminTab({ onTokenSave }) { | |
| const [adminLoginResp, setAdminLoginResp] = useState(null); | |
| const [statsData, setStatsData] = useState(null); | |
| const [emailData, setEmailData] = useState(null); | |
| const [adminUserResp, setAdminUserResp] = useState(null); | |
| const [adminRiskResp, setAdminRiskResp] = useState(null); | |
| const [statsResp, setStatsResp] = useState(null); | |
| const [adminUserId, setAdminUserId] = useState(''); | |
| const [loading, setLoading] = useState({}); | |
| const setLoad = (k, v) => setLoading(p => ({ ...p, [k]: v })); | |
| const quickAdminLogin = async () => { | |
| setLoad('login', true); | |
| const r = await req(`${AUTH}/login`, 'POST', { email: 'demo.admin@adaptive.demo', password: 'Admin@Demo456!' }, false); | |
| setAdminLoginResp(r); | |
| if (r.ok && r.data?.access_token) { saveToken(r.data.access_token); onTokenSave?.(); } | |
| setLoad('login', false); | |
| }; | |
| const loadAdminStats = async () => { | |
| setLoad('stats', true); | |
| const r = await req(`${ADMIN}/statistics`); | |
| if (r.ok) setStatsData(r.data); | |
| setLoad('stats', false); | |
| }; | |
| const checkEmailStatus = async () => { | |
| setLoad('email', true); | |
| const r = await req(`${ADMIN}/email-status`); | |
| if (r.ok) setEmailData(r.data); | |
| else setEmailData(null); | |
| setLoad('email', false); | |
| }; | |
| const userCall = async (url, method = 'GET') => { | |
| setLoad('user', true); | |
| setAdminUserResp(await req(url, method)); | |
| setLoad('user', false); | |
| }; | |
| const riskCall = async (url) => { | |
| setLoad('risk', true); | |
| setAdminRiskResp(await req(url)); | |
| setLoad('risk', false); | |
| }; | |
| const adminBlock = async () => { | |
| if (!adminUserId) { alert('Enter user ID.'); return; } | |
| await userCall(`${ADMIN}/users/${adminUserId}/block`, 'POST'); | |
| }; | |
| const adminUnblock = async () => { | |
| if (!adminUserId) { alert('Enter user ID.'); return; } | |
| await userCall(`${ADMIN}/users/${adminUserId}/unblock`, 'POST'); | |
| }; | |
| const STAT_ITEMS = [ | |
| { key: 'total_users', label: 'Total Users', color: 'var(--info)' }, | |
| { key: 'active_sessions', label: 'Active Sessions',color: 'var(--success)' }, | |
| { key: 'high_risk_events_today',label: 'High Risk Today',color: 'var(--danger)' }, | |
| { key: 'failed_logins_today', label: 'Failed Logins', color: 'var(--warn)' }, | |
| ]; | |
| return ( | |
| <div> | |
| <Callout type="warn"> | |
| Admin endpoints require an admin JWT token. Login with{' '} | |
| <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code> first. | |
| </Callout> | |
| {/* Quick Admin Login */} | |
| <Card> | |
| <CardHeader icon="π">Quick Admin Login</CardHeader> | |
| <div className="flex items-center gap-3 flex-wrap"> | |
| <span className="text-sm text-2"> | |
| <code>demo.admin@adaptive.demo</code> / <code>Admin@Demo456!</code> | |
| </span> | |
| <button className="btn btn-primary btn-sm" onClick={quickAdminLogin} disabled={loading.login}> | |
| {loading.login ? 'β¦' : 'Login as Admin'} | |
| </button> | |
| </div> | |
| <ResponseBox result={adminLoginResp} /> | |
| </Card> | |
| {/* Stats */} | |
| <div className="flex items-center gap-3 mb-3"> | |
| <button className="btn btn-ghost btn-sm" onClick={loadAdminStats} disabled={loading.stats}> | |
| {loading.stats ? 'β¦' : 'π Load Statistics'} | |
| </button> | |
| </div> | |
| <div className="grid-4 mb-4"> | |
| {STAT_ITEMS.map(s => ( | |
| <div className="stat-box" key={s.key}> | |
| <div className="stat-num" style={{ color: s.color }}> | |
| {statsData ? (statsData[s.key] ?? 'β') : 'β'} | |
| </div> | |
| <div className="stat-label">{s.label}</div> | |
| </div> | |
| ))} | |
| </div> | |
| {/* Email Status */} | |
| <Card> | |
| <CardHeader | |
| icon="βοΈ" | |
| actions={ | |
| <button className="btn btn-ghost btn-sm" onClick={checkEmailStatus} disabled={loading.email}> | |
| {loading.email ? 'β¦' : 'π Refresh'} | |
| </button> | |
| } | |
| > | |
| Email Service Status | |
| </CardHeader> | |
| <EmailStatusBody data={emailData} /> | |
| </Card> | |
| <div className="grid-2"> | |
| {/* User Management */} | |
| <Card> | |
| <CardHeader icon="π₯">User Management</CardHeader> | |
| <div className="flex flex-wrap gap-2 mb-3"> | |
| <button className="btn btn-ghost btn-sm" onClick={() => userCall(`${ADMIN}/users`)}>List Users</button> | |
| <button className="btn btn-ghost btn-sm" onClick={() => userCall(`${ADMIN}/sessions`)}>List Sessions</button> | |
| </div> | |
| <FormGroup label="User ID"> | |
| <input type="number" value={adminUserId} onChange={e => setAdminUserId(e.target.value)} placeholder="User ID" /> | |
| </FormGroup> | |
| <div className="flex gap-2"> | |
| <button className="btn btn-success btn-sm flex-1" onClick={adminUnblock} disabled={loading.user}>Unblock</button> | |
| <button className="btn btn-danger btn-sm flex-1" onClick={adminBlock} disabled={loading.user}>Block</button> | |
| </div> | |
| <ResponseBox result={adminUserResp} /> | |
| </Card> | |
| {/* Risk Events */} | |
| <Card> | |
| <CardHeader icon="β οΈ">Risk Events & Anomalies</CardHeader> | |
| <div className="flex flex-wrap gap-2 mb-3"> | |
| <button className="btn btn-warn btn-sm" onClick={() => riskCall(`${ADMIN}/risk-events`)} disabled={loading.risk}>Risk Events</button> | |
| <button className="btn btn-danger btn-sm" onClick={() => riskCall(`${ADMIN}/anomalies`)} disabled={loading.risk}>Active Anomalies</button> | |
| <button className="btn btn-ghost btn-sm" onClick={() => riskCall(`${RISK}/overview`)} disabled={loading.risk}>Overview</button> | |
| </div> | |
| <ResponseBox result={adminRiskResp} /> | |
| </Card> | |
| </div> | |
| {/* Risk Stats */} | |
| <Card> | |
| <CardHeader icon="π">Risk Statistics</CardHeader> | |
| <div className="flex gap-2 mb-3"> | |
| {['day','week','month'].map(p => ( | |
| <button key={p} className="btn btn-ghost btn-sm" onClick={async () => { | |
| setLoad('period', true); | |
| setStatsResp(await req(`${ADMIN}/risk-statistics?period=${p}`)); | |
| setLoad('period', false); | |
| }} disabled={loading.period}> | |
| {p.charAt(0).toUpperCase() + p.slice(1)} | |
| </button> | |
| ))} | |
| </div> | |
| <ResponseBox result={statsResp} /> | |
| </Card> | |
| </div> | |
| ); | |
| } | |