import { renderHeader } from '../components/header.js'; import { authService, clearAuthMessages, setAuthBusy, setAuthError, state, } from '../state.js'; import { toUserMessage } from '../services/ui-error-texts.js'; import { composePasswordFromWords, emptyPasswordWords, normalizePasswordWords, PASSWORD_MAX_LENGTH, PASSWORD_WORDS_COUNT, } from '../services/password-words.js'; function createWordsLayout({ words, onInput }) { const section = document.createElement('div'); section.className = 'registration-words-block'; const grid = document.createElement('div'); grid.className = 'registration-words-grid'; const inputs = Array.from({ length: PASSWORD_WORDS_COUNT }, (_, index) => { const row = document.createElement('label'); row.className = 'registration-word-row'; const number = document.createElement('span'); number.className = 'registration-word-number'; number.textContent = `${index + 1}.`; const input = document.createElement('input'); input.className = 'input registration-word-input'; input.type = 'text'; input.autocomplete = 'off'; input.autocapitalize = 'off'; input.spellcheck = false; input.maxLength = 32; input.value = words[index]; input.addEventListener('input', () => onInput(index, input.value)); row.append(number, input); grid.append(row); return input; }); const hint = document.createElement('p'); hint.className = 'meta-muted'; hint.textContent = 'Можно вводить любые слова на любых языках. Можно заполнить не все 12 полей. В конце они просто склеиваются в один пароль длиной до 256 символов.'; const preview = document.createElement('p'); preview.className = 'status-line'; section.append(grid, hint); return { section, inputs, preview }; } export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false }; export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; clearAuthMessages(); const form = document.createElement('div'); form.className = 'card stack'; let passwordMode = String(state.loginDraft.passwordMode || 'single') === 'words' ? 'words' : 'single'; let passwordWords = normalizePasswordWords(state.loginDraft.passwordWords); const loginInput = document.createElement('input'); loginInput.className = 'input'; loginInput.type = 'text'; loginInput.autocomplete = 'off'; loginInput.autocapitalize = 'off'; loginInput.spellcheck = false; loginInput.value = state.loginDraft.login; loginInput.placeholder = 'Введите логин'; const passwordInput = document.createElement('input'); passwordInput.className = 'input'; passwordInput.type = 'password'; passwordInput.name = 'shine-login-password'; passwordInput.autocomplete = 'new-password'; passwordInput.autocapitalize = 'off'; passwordInput.spellcheck = false; passwordInput.maxLength = PASSWORD_MAX_LENGTH; passwordInput.value = passwordMode === 'single' ? state.loginDraft.password : ''; passwordInput.placeholder = 'Введите пароль'; const { section: wordsSection, inputs: wordInputs, preview: wordsPreview, } = createWordsLayout({ words: passwordWords, onInput: (index, value) => { passwordWords[index] = value; syncDraftState(); updateWordsPreview(); }, }); const passwordModeToggle = document.createElement('label'); passwordModeToggle.className = 'registration-toggle'; const passwordModeCheckbox = document.createElement('input'); passwordModeCheckbox.type = 'checkbox'; passwordModeCheckbox.checked = passwordMode === 'words'; const passwordModeLabel = document.createElement('span'); passwordModeLabel.textContent = 'Представить пароль в виде 12 слов'; passwordModeToggle.append(passwordModeCheckbox, passwordModeLabel); const hint = document.createElement('p'); hint.className = 'meta-muted'; hint.textContent = 'Введите логин. На следующем шаге сохраните ключи на устройстве.'; const status = document.createElement('p'); status.className = 'status-line is-unavailable'; status.style.display = 'none'; let passwordField = null; const passwordLengthText = document.createElement('p'); passwordLengthText.className = 'status-line'; function getCurrentPassword() { return passwordMode === 'words' ? composePasswordFromWords(passwordWords) : String(passwordInput.value || ''); } function syncDraftState() { state.loginDraft.login = loginInput.value.trim(); state.loginDraft.passwordMode = passwordMode; state.loginDraft.passwordWords = normalizePasswordWords(passwordWords); state.loginDraft.password = getCurrentPassword(); } function updateWordsPreview() { const password = getCurrentPassword(); const text = `Итоговая длина пароля: ${password.length} символов.`; wordsPreview.textContent = text; passwordLengthText.textContent = text; } function updatePasswordModeVisibility() { const wordsMode = passwordMode === 'words'; wordsSection.style.display = wordsMode ? 'grid' : 'none'; if (passwordField) passwordField.style.display = wordsMode ? 'none' : 'grid'; passwordInput.style.display = wordsMode ? 'none' : ''; updateWordsPreview(); } form.innerHTML = ` `; form.children[0].append(loginInput); passwordField = form.children[1]; passwordField.append(passwordInput); form.append(passwordModeToggle, wordsSection, passwordLengthText, hint, status); updatePasswordModeVisibility(); syncDraftState(); loginInput.addEventListener('input', syncDraftState); passwordInput.addEventListener('input', () => { syncDraftState(); updateWordsPreview(); }); passwordModeCheckbox.addEventListener('change', () => { const nextMode = passwordModeCheckbox.checked ? 'words' : 'single'; if (nextMode === passwordMode) return; if (nextMode === 'words') { passwordWords = emptyPasswordWords(); wordInputs.forEach((input) => { input.value = ''; }); passwordInput.value = ''; } else { passwordInput.value = composePasswordFromWords(passwordWords); } passwordMode = nextMode; updatePasswordModeVisibility(); updateWordsPreview(); syncDraftState(); }); 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('start-view')); const enterButton = document.createElement('button'); enterButton.className = 'primary-btn'; enterButton.type = 'button'; enterButton.textContent = 'Войти'; enterButton.addEventListener('click', async () => { status.style.display = 'none'; syncDraftState(); if (!state.loginDraft.login) { status.textContent = 'Введите логин.'; status.style.display = ''; return; } if (state.loginDraft.password.length > PASSWORD_MAX_LENGTH) { status.textContent = `Пароль слишком длинный. Максимальная длина: ${PASSWORD_MAX_LENGTH} символов.`; status.style.display = ''; return; } setAuthBusy(true); setAuthError(''); enterButton.disabled = true; enterButton.textContent = 'Входим...'; try { await authService.reconnect(state.entrySettings.shineServer); const result = await authService.createSessionForExistingUser(state.loginDraft.login, state.loginDraft.password); state.registrationDraft.flowType = 'login'; state.registrationDraft.login = result.login; state.registrationDraft.password = state.loginDraft.password; state.registrationDraft.passwordMode = state.loginDraft.passwordMode; state.registrationDraft.passwordWords = normalizePasswordWords(state.loginDraft.passwordWords); state.registrationDraft.sessionId = result.sessionId; state.registrationDraft.storagePwd = result.storagePwd; state.registrationDraft.pendingKeyBundle = result.keyBundle; state.registrationDraft.pendingSessionMaterial = result.sessionMaterial; navigate('registration-keys-view'); } catch (error) { const message = toUserMessage(error, 'Не удалось выполнить вход.'); setAuthError(message); status.textContent = message; status.style.display = ''; } finally { setAuthBusy(false); enterButton.disabled = false; enterButton.textContent = 'Войти'; } }); actions.append(backButton, enterButton); screen.append( renderHeader({ title: 'Войти по логину', leftAction: { label: '←', onClick: () => navigate('start-view') }, }), form, actions, ); return screen; }