230 lines
15 KiB
JavaScript
230 lines
15 KiB
JavaScript
import { renderHeader } from '../components/header.js';
|
||
import { state } from '../state.js';
|
||
|
||
export const pageMeta = { id: 'registration-faq-view', title: 'Вопросы о регистрации', showAppChrome: false };
|
||
|
||
export const REGISTRATION_FAQ_TOPICS = [
|
||
{
|
||
id: 'keys-storage',
|
||
shortTitle: 'Где ключи',
|
||
title: 'У кого хранятся ключи?',
|
||
paragraphs: [
|
||
'Ключи хранятся только у вас: на вашем устройстве, на доверенных устройствах или на отдельном внешнем устройстве, которое вы контролируете сами.',
|
||
'SHiNE не хранит ваши приватные ключи на сервере. Сервер помогает с доставкой и синхронизацией, но не владеет вашим секретом.',
|
||
'Если захотите, ключи можно держать на отдельном полностью программируемом устройстве с открытым кодом, например на ESP32-контроллере.',
|
||
],
|
||
},
|
||
{
|
||
id: 'reliability',
|
||
shortTitle: 'Надёжность',
|
||
title: 'Насколько это надёжно?',
|
||
paragraphs: [
|
||
'Мы делаем ставку на открытость: клиентский код открыт, серверный код открыт, протокол открыт. Это позволяет проверять систему независимо, а не верить обещаниям на слово.',
|
||
'Мы рекомендуем использовать браузеры с открытым исходным кодом. Позже планируются отдельные приложения для Android, iPhone, Ubuntu Touch и Linux, тоже с открытым кодом.',
|
||
'Проект распространяется по лицензии AGPL v3. Часть важных данных и регистрационных записей также опирается на блокчейн-слой, чтобы уменьшать зависимость от одной закрытой стороны.',
|
||
],
|
||
},
|
||
{
|
||
id: 'key-derivation',
|
||
shortTitle: 'Деривация',
|
||
title: 'Как генерируются ключи и что делает пароль?',
|
||
paragraphs: [
|
||
'Из вашего логина и пароля с помощью Argon2id вычисляется специальный секрет.',
|
||
'Уже из этого секрета детерминированно строятся три основных ключа: root key, blockchain key и device key.',
|
||
'Это значит, что логин и пароль не просто проверяются на сервере, а реально участвуют в создании ваших ключей. У разных логинов даже с одинаковым паролем будут разные ключи.',
|
||
],
|
||
},
|
||
{
|
||
id: 'three-keys',
|
||
shortTitle: 'Три ключа',
|
||
title: 'Зачем нужны три ключа?',
|
||
paragraphs: [
|
||
'Root key нужен для управления вашей основной публичной записью и важными изменениями личности, включая обновление главной публичной части в Solana.',
|
||
'Blockchain key нужен для подписания действий и записей в блокчейне SHiNE.',
|
||
'Device key нужен для входов и работы конкретного устройства. Благодаря разделению ключей можно точнее выдавать права одним устройствам и не выдавать другим.',
|
||
'Если не хочется в это вникать, обычно можно просто сохранить все ключи на своём устройстве. Для большинства обычных сценариев на iPhone, Android и Linux это вполне практично. Для больших сумм или повышенного риска лучше отдельное внешнее устройство.',
|
||
],
|
||
},
|
||
{
|
||
id: 'generation-time',
|
||
shortTitle: 'Зачем время',
|
||
title: 'Зачем нужна заметная пауза при генерации?',
|
||
paragraphs: [
|
||
'Генерация специально сделана не мгновенной. Это усложняет массовый подбор паролей.',
|
||
'Argon2id расходует и время, и память, поэтому атаки на GPU и видеокартах становятся заметно дороже и медленнее.',
|
||
'Небольшая задержка при создании секрета здесь работает как дополнительная защита.',
|
||
],
|
||
},
|
||
{
|
||
id: 'strong-password',
|
||
shortTitle: 'Какой пароль',
|
||
title: 'Какой пароль считается надёжным?',
|
||
paragraphs: [
|
||
'Минимально разумный уровень сейчас начинается примерно от 8 символов.',
|
||
'Хороший практический ориентир для большинства людей: 12 символов и больше. Пароль у нас может быть длиной до 256 символов.',
|
||
'Если вам удобнее думать словами, можно использовать режим из 12 полей ниже: слова просто склеиваются в один длинный пароль, и система не проверяет орфографию.',
|
||
],
|
||
},
|
||
{
|
||
id: 'one-or-twelve',
|
||
shortTitle: '1 или 12 слов',
|
||
title: 'Чем отличается один пароль от режима 12 слов?',
|
||
paragraphs: [
|
||
'Технически ничем: это один и тот же пароль. Режим 12 слов нужен только для удобства запоминания и ввода.',
|
||
'Можно заполнить все 12 полей, можно только первые 6, можно использовать слова от другого кошелька, разные языки и любые нестандартные символы.',
|
||
'Главное помнить, что в конце всё равно получается одна строка длиной до 256 символов.',
|
||
],
|
||
},
|
||
{
|
||
id: 'why-own-password',
|
||
shortTitle: 'Зачем свой',
|
||
title: 'Почему лучше иметь свой пароль и свои ключи?',
|
||
paragraphs: [
|
||
'Чем дальше, тем проще будет подделывать фотографию, голос, интонацию и даже другие привычные признаки личности с помощью нейросетей.',
|
||
'На расстоянии всё сложнее будет понять, что перед вами действительно вы, если опираться только на внешние признаки.',
|
||
'Поэтому персональные ключи, которые храните только вы, становятся надёжнее, чем зависимость от сторонней организации, которая держит ключи у себя.',
|
||
],
|
||
},
|
||
{
|
||
id: 'first-server',
|
||
shortTitle: 'Первый сервер',
|
||
title: 'Что такое первый сервер SHiNE?',
|
||
paragraphs: [
|
||
'Первый сервер SHiNE это тот сервер, на который вам будут писать и звонить в самом начале. При регистрации он записывается как ваш первый сервер доступа.',
|
||
'Позже вы сможете сменить сервер, а ваши данные останутся с вами. В будущем серверов может быть несколько одновременно.',
|
||
'Если серверов несколько, данные между ними будут синхронизироваться автоматически. Если добавляете новый сервер и убираете старый, просто дождитесь завершения синхронизации перед отключением старого.',
|
||
'Если у вас не остаётся ни одного сервера, синхронизации, конечно, не будет, пока не появится хотя бы один активный сервер снова.',
|
||
],
|
||
},
|
||
{
|
||
id: 'hardware-device',
|
||
shortTitle: 'ESP32',
|
||
title: 'Нужно ли отдельное устройство для ключей?',
|
||
paragraphs: [
|
||
'Идеальный вариант для важных ключей: отдельное физическое устройство, которое вы контролируете сами.',
|
||
'Если пока не хотите покупать отдельное устройство, можно пользоваться телефоном. Но отдельный контроллер или мини-устройство обычно даёт лучший контроль и более понятную модель доверия.',
|
||
'Красивая готовая модель Waveshare на ESP32-S3 Touch AMOLED 2.16 стоит около 32 долларов. Есть и более дешёвые варианты на открытых чипах, примерно от 10 до 15 долларов.',
|
||
'Если у вас другая модель, под неё можно адаптировать открытую прошивку. Для простых переносов это реально сделать довольно быстро.',
|
||
],
|
||
links: [
|
||
{
|
||
label: 'Документация Waveshare ESP32-S3 Touch AMOLED 2.16',
|
||
href: 'https://docs.waveshare.com/ESP32-S3-Touch-AMOLED-2.16',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
id: 'wallet-device',
|
||
shortTitle: 'Кошелёк',
|
||
title: 'Можно ли использовать такое устройство как кошелёк?',
|
||
paragraphs: [
|
||
'Да. Идея SHiNE в том, что устройство может подписывать не только внутренние действия, но и любые другие данные, если для этого добавлена нужная логика.',
|
||
'То есть это направление совместимо с моделью аппаратного кошелька: вы храните ключи у себя, а устройство подписывает то, что вы разрешили.',
|
||
'Пока ещё не все валюты и сценарии доведены до готового пользовательского уровня, но архитектурно это именно путь к универсальному подписывающему устройству.',
|
||
],
|
||
},
|
||
];
|
||
|
||
function getTopicById(topicId) {
|
||
return REGISTRATION_FAQ_TOPICS.find((topic) => topic.id === topicId) || REGISTRATION_FAQ_TOPICS[0];
|
||
}
|
||
|
||
export function openRegistrationFaq(navigate, topicId) {
|
||
state.registrationHelp.selectedTopic = getTopicById(topicId).id;
|
||
navigate('registration-faq-view');
|
||
}
|
||
|
||
export function render({ navigate }) {
|
||
const selectedTopic = getTopicById(state.registrationHelp?.selectedTopic);
|
||
const screen = document.createElement('section');
|
||
screen.className = 'stack';
|
||
|
||
const heroCard = document.createElement('div');
|
||
heroCard.className = 'card stack registration-faq-hero';
|
||
heroCard.innerHTML = `
|
||
<div class="badge alt">Вопросы о регистрации</div>
|
||
<p class="auth-copy">Короткие ответы на самые частые вопросы о ключах, пароле, первом сервере и доверенных устройствах.</p>
|
||
`;
|
||
|
||
const topicCard = document.createElement('div');
|
||
topicCard.className = 'card stack registration-faq-topic';
|
||
|
||
const question = document.createElement('h2');
|
||
question.className = 'registration-faq-title';
|
||
question.textContent = selectedTopic.title;
|
||
topicCard.append(question);
|
||
|
||
selectedTopic.paragraphs.forEach((paragraph) => {
|
||
const p = document.createElement('p');
|
||
p.className = 'auth-copy';
|
||
p.textContent = paragraph;
|
||
topicCard.append(p);
|
||
});
|
||
|
||
if (Array.isArray(selectedTopic.links) && selectedTopic.links.length > 0) {
|
||
selectedTopic.links.forEach((linkItem) => {
|
||
const link = document.createElement('a');
|
||
link.className = 'link-card';
|
||
link.href = linkItem.href;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener noreferrer';
|
||
link.textContent = linkItem.label;
|
||
topicCard.append(link);
|
||
});
|
||
}
|
||
|
||
const topicsCard = document.createElement('div');
|
||
topicsCard.className = 'card stack';
|
||
|
||
const topicsLabel = document.createElement('p');
|
||
topicsLabel.className = 'field-label';
|
||
topicsLabel.textContent = 'Другие вопросы';
|
||
|
||
const topicsGrid = document.createElement('div');
|
||
topicsGrid.className = 'registration-faq-grid';
|
||
|
||
REGISTRATION_FAQ_TOPICS.forEach((topic) => {
|
||
const button = document.createElement('button');
|
||
button.className = topic.id === selectedTopic.id ? 'secondary-btn' : 'ghost-btn';
|
||
button.type = 'button';
|
||
button.textContent = topic.shortTitle;
|
||
button.addEventListener('click', () => {
|
||
state.registrationHelp.selectedTopic = topic.id;
|
||
navigate('registration-faq-view');
|
||
});
|
||
topicsGrid.append(button);
|
||
});
|
||
|
||
topicsCard.append(topicsLabel, topicsGrid);
|
||
|
||
const actions = document.createElement('div');
|
||
actions.className = 'auth-footer-actions';
|
||
|
||
const backButton = document.createElement('button');
|
||
backButton.className = 'ghost-btn';
|
||
backButton.type = 'button';
|
||
backButton.textContent = 'Назад';
|
||
backButton.addEventListener('click', () => navigate('register-view'));
|
||
|
||
const registerButton = document.createElement('button');
|
||
registerButton.className = 'primary-btn';
|
||
registerButton.type = 'button';
|
||
registerButton.textContent = 'К регистрации';
|
||
registerButton.addEventListener('click', () => navigate('register-view'));
|
||
|
||
actions.append(backButton, registerButton);
|
||
|
||
screen.append(
|
||
renderHeader({
|
||
title: 'Вопросы о регистрации',
|
||
leftAction: { label: '←', onClick: () => navigate('register-view') },
|
||
}),
|
||
heroCard,
|
||
topicCard,
|
||
topicsCard,
|
||
actions,
|
||
);
|
||
|
||
return screen;
|
||
}
|