// ════════════════════════════════════════════════════════════════
// 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 */}
setTab('profile')}>профиль
setTab('sessions')}>сессии
setTab('security')}>безопасность
{sub && !sub.isInternal && (
onChangeRoute && onChangeRoute('account.billing')}>
подписка
{tier.name}
)}
>
}
right={
Выйти
}
/>
{tab === 'profile' && (
{/* Left */}
setNewName(e.target.value)} style={authFieldStyle} />
Сохранить
.',
why: 'Регенерация нужна если токен утёк или вы меняете команду. Старый перестанет работать сразу, во всех клиентах надо подменить вручную. Делайте это перед запуском новых интеграций, не во время рабочего дня.',
}}>
} />
{daysLeft} дн — продлите
: `${daysLeft} дн осталось`
)} />
{newToken && (
)}
{!newToken && (
{showRegen ? (
Точно? Старый токен перестанет работать. Все интеграции, использующие его, начнут возвращать 401.
setShowRegen(false)}>Отмена
Подтверждаю
) : (
setShowRegen(true)}>
Регенерировать токен
)}
)}
{/* 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 && (
onChangeRoute && onChangeRoute('account.billing')}>
Подробнее о подписке
)}
)}
)}
{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 (
Экспорт моих данных (JSON)
Удалить мою учётную запись
);
}
window.AccountPage = AccountPage;