// ════════════════════════════════════════════════════════════════ // InviteRedeemPage — публичный, без auth, принятие приглашения // /invite/ → POST /api/invites//redeem → api_token (один раз) // ════════════════════════════════════════════════════════════════ function InviteRedeemPage({ inviteToken, onAccepted, onLogin }) { // Find mock invite const invite = window.SDH_ADMIN.invites.find(i => i.token === inviteToken) || window.SDH_ADMIN.invites.find(i => i.status === 'pending'); // fallback first pending const inviter = invite && window.SDH_ADMIN.users.find(u => u.name === invite.inviter); const [name, setName] = React.useState(''); const [password, setPassword] = React.useState(''); const [confirm, setConfirm] = React.useState(''); const [agree, setAgree] = React.useState(false); const [submitting, setSubmitting] = React.useState(false); const [accepted, setAccepted] = React.useState(false); const [apiToken, setApiToken] = React.useState(null); const [error, setError] = React.useState(null); if (!invite) { return (
приглашение не найдено

Эта ссылка уже не работает

Возможно, приглашение было отозвано, истёк его срок (7 дней) или уже принято на другом устройстве. Свяжитесь с человеком, который вас приглашал, чтобы получить новую ссылку.

); } // Password strength (mock: length-based) const pwStrength = password.length === 0 ? 0 : password.length < 8 ? 0.25 : password.length < 12 ? 0.5 : /[A-Z]/.test(password) && /[0-9]/.test(password) && /[^a-zA-Z0-9]/.test(password) ? 1 : 0.75; async function submit(e) { e.preventDefault(); if (password !== confirm) { setError('Пароли не совпадают'); return; } if (password.length < 12) { setError('Минимум 12 символов'); return; } if (!agree) { setError('Необходимо согласие'); return; } if (!name || name.trim().length < 2) { setError('Имя обязательно'); return; } setError(null); setSubmitting(true); // ── API mode: real backend ── if (window.SDH_USE_API && window.SDH_HYDRATE) { try { const resp = await window.SDH_HYDRATE.redeemInvite(inviteToken, name.trim(), password); setApiToken(resp.api_token); setAccepted(true); setSubmitting(false); } catch (err) { let msg = err && err.message; if (err && err.status === 410) msg = 'Срок приглашения истёк. Запросите новую ссылку.'; else if (err && err.status === 404) msg = 'Приглашение не найдено или уже принято.'; setError(msg || 'Ошибка принятия приглашения'); setSubmitting(false); } return; } // ── Mock mode ── setTimeout(() => { // Generate mock API token const t = 'mcp_' + Math.random().toString(36).slice(2, 8) + Math.random().toString(36).slice(2, 8) + '_' + Date.now().toString(36); setApiToken(t); setAccepted(true); setSubmitting(false); }, 600); } const roleCfg = window.SDH_ADMIN.ROLES.find(r => r.id === invite.role); if (accepted) { return (
приглашение принято

Добро пожаловать в систему

Вам назначена роль . Ниже — ваш Bearer token для API доступа. Сохраните его в менеджер паролей сейчас, иначе придётся регенерировать.

); } return (
приглашение в symbiosis.datahub

Добро пожаловать

Вас пригласил {invite.inviter} как . Завершите регистрацию, чтобы получить доступ.

{/* Role explainer */}
что значит роль

{roleCfg.what}

setName(e.target.value)} autoFocus style={authFieldStyle} placeholder="например: Иван Шевцов" /> setPassword(e.target.value)} style={authFieldStyle} placeholder="придумайте надёжный" /> {password && (
0.5 ? 'var(--warning)' : 'var(--danger)', transition: 'width .15s', }} />
{pwStrength === 1 ? 'надёжный' : pwStrength > 0.5 ? 'нормально' : 'слабый'}
)} setConfirm(e.target.value)} style={authFieldStyle} /> {error && (
{error}
)}
); } const inviteShellStyle = { minHeight: '100vh', background: 'linear-gradient(180deg, var(--hue-emerald-subtle), var(--surface-canvas) 50%)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 'var(--space-8) var(--space-4)', }; const inviteCardStyle = { width: '100%', maxWidth: 500, background: 'var(--surface-elevated)', borderRadius: 'var(--radius-2xl)', border: '1px solid var(--border-subtle)', boxShadow: 'var(--shadow-lg)', padding: 'var(--space-8)', }; const inviteH1Style = { margin: '0 0 var(--space-3)', fontSize: 32, fontWeight: 600, letterSpacing: '-0.025em', color: 'var(--text-primary)', lineHeight: 1.1, }; const inviteEmStyle = { fontFamily: 'var(--font-display)', fontStyle: 'italic', color: 'var(--brand-bronze-deep)', fontWeight: 600, }; window.InviteRedeemPage = InviteRedeemPage;