// ════════════════════════════════════════════════════════════════ // AccountPage — /account — профиль пользователя // Любой залогиненный (включая viewer) // ════════════════════════════════════════════════════════════════ function AccountPage({ user, onLogout, onChangeRoute }) { const [tab, setTab] = React.useState('profile'); // profile | sessions | security const [newName, setNewName] = React.useState(user.name); const [showRegen, setShowRegen] = React.useState(false); const [newToken, setNewToken] = React.useState(null); const ttl = window.SDH_ADMIN.TTL[user.role]; const daysLeft = user.days_left; const sub = window.SDH_BILLING.getSub(user.id); const tier = sub ? window.SDH_BILLING.getTier(sub.tierCode) : null; const usage = sub ? window.SDH_BILLING.buildUsage(user.id, sub.tierCode) : null; async function regen() { // API mode if (window.SDH_USE_API && window.SDH_API) { try { const resp = await window.SDH_API.post('/api/auth/regenerate-token'); if (resp && resp.token) { setNewToken(resp.token); window.SDH_API.setBearer(resp.token); } } catch (e) { alert('Ошибка регенерации: ' + (e.message || e)); } setShowRegen(false); return; } // Mock const t = 'mcp_' + Math.random().toString(36).slice(2, 8) + Math.random().toString(36).slice(2, 8) + '_' + Date.now().toString(36); setNewToken(t); setShowRegen(false); } return (
{/* Tab strip */} {sub && !sub.isInternal && ( )} } right={ } />
{tab === 'profile' && (
{/* Left */}
{user.name}
setNewName(e.target.value)} style={authFieldStyle} />
.', why: 'Регенерация нужна если токен утёк или вы меняете команду. Старый перестанет работать сразу, во всех клиентах надо подменить вручную. Делайте это перед запуском новых интеграций, не во время рабочего дня.', }}>
} /> {daysLeft} дн — продлите : `${daysLeft} дн осталось` )} />
{newToken && (
)} {!newToken && (
{showRegen ? (
Точно? Старый токен перестанет работать. Все интеграции, использующие его, начнут возвращать 401.
) : ( )}
)}
{/* Right */}
} /> {user.note && }
{sub && (
{tier.name} {sub.isInternal && internal} {!sub.isInternal && {tier.priceRub.toLocaleString('ru-RU')} ₽/мес}
{usage && tier.tokens > 0 && (
токенов в этом месяце
)} {sub.isInternal && (

Внутренний tier команды Symbiosis. Бессрочный, без лимита.

)} {!sub.isInternal && ( )}
)}
)} {tab === 'sessions' && } {tab === 'security' && }
); } function Panel({ title, help, children }) { return (

{help ? {title} : title}

{children}
); } function Fact({ label, value, mono }) { return (
{label} {value}
); } function fmtAgo(iso) { const d = new Date(iso); const mins = Math.round((new Date('2026-05-23T10:00:00Z') - d) / 60000); if (mins < 60) return `${mins} мин назад`; if (mins < 24 * 60) return `${Math.round(mins / 60)} ч назад`; return `${Math.round(mins / 60 / 24)} д назад`; } function SessionsTab({ user }) { // mock 6 sessions const sessions = [ { id: 's1', device: 'macOS · Firefox 124', ip: '188.187.143.221', startedAt: 'сегодня 09:42', current: true }, { id: 's2', device: 'iOS · Safari', ip: '188.187.143.221', startedAt: 'вчера 18:14' }, { id: 's3', device: 'macOS · Firefox 124', ip: '188.187.143.221', startedAt: '21.05 11:08' }, { id: 's4', device: 'macOS · Firefox 124', ip: '95.84.10.4', startedAt: '18.05 22:30', note: 'другая сеть' }, { id: 's5', device: 'MCP client', ip: 'agent', startedAt: 'программатически', api: true }, ]; return (
{sessions.map(s => (
{s.device} {s.current && текущая} {s.api && api}
{s.note &&
{s.note}
}
{s.ip} {s.startedAt} {!s.current && ( )}
))}
); } function SecurityTab({ user }) { const [oldPw, setOldPw] = React.useState(''); const [newPw, setNewPw] = React.useState(''); const [confirmPw, setConfirmPw] = React.useState(''); const [pwBusy, setPwBusy] = React.useState(false); const [pwMsg, setPwMsg] = React.useState(null); async function changePassword() { setPwMsg(null); if (newPw !== confirmPw) { setPwMsg({ type: 'error', text: 'Пароли не совпадают' }); return; } if (newPw.length < 12) { setPwMsg({ type: 'error', text: 'Минимум 12 символов' }); return; } setPwBusy(true); if (window.SDH_USE_API && window.SDH_API) { try { await window.SDH_API.post('/api/auth/change-password', { old: oldPw, new: newPw }); setPwMsg({ type: 'ok', text: 'Пароль изменён. Другие сессии завершены.' }); setOldPw(''); setNewPw(''); setConfirmPw(''); } catch (e) { const msg = e && e.status === 401 ? 'Неверный старый пароль' : (e.message || 'Ошибка'); setPwMsg({ type: 'error', text: msg }); } setPwBusy(false); return; } // Mock setTimeout(() => { setPwMsg({ type: 'ok', text: 'Пароль изменён (mock).' }); setOldPw(''); setNewPw(''); setConfirmPw(''); setPwBusy(false); }, 400); } return (
setOldPw(e.target.value)} style={authFieldStyle} /> setNewPw(e.target.value)} style={authFieldStyle} /> setConfirmPw(e.target.value)} style={authFieldStyle} /> {pwMsg && (
{pwMsg.text}
)}
); } window.AccountPage = AccountPage;