Files
SHiNE-server/shine-UI/js/pages/login-password-view.js
T

184 lines
6.2 KiB
JavaScript

import { createTopBar } from '../components/topbar.js';
import {
authService,
clearAuthMessages,
setAuthBusy,
setAuthError,
state,
} from '../state.js';
import { toUserMessage } from '../services/ui-error-texts.js';
import { emptyPasswordWords, PASSWORD_MAX_LENGTH } from '../services/password-words.js';
export const pageMeta = { id: 'login-password-view', title: 'Введите пароль', showAppChrome: false };
function setStatus(statusEl, message) {
statusEl.textContent = message;
statusEl.style.display = message ? '' : 'none';
}
function createSecretOverlay() {
const overlay = document.createElement('div');
overlay.className = 'secret-generation-overlay';
overlay.style.display = 'none';
const card = document.createElement('div');
card.className = 'secret-generation-card stack';
const spinner = document.createElement('div');
spinner.className = 'secret-generation-spinner';
spinner.setAttribute('aria-hidden', 'true');
const title = document.createElement('div');
title.className = 'secret-generation-title';
title.textContent = 'Генерируем секрет…';
const progress = document.createElement('div');
progress.className = 'secret-generation-progress';
progress.textContent = '';
card.append(spinner, title, progress);
overlay.append(card);
return { overlay, progress };
}
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack auth-screen auth-screen--lower';
clearAuthMessages();
const login = String(state.loginDraft.login || '').trim();
if (!login) {
window.setTimeout(() => navigate('login-view'), 0);
}
const panel = document.createElement('section');
panel.className = 'login-panel stack';
const title = document.createElement('h1');
title.className = 'login-panel-title';
title.textContent = 'Введите пароль';
const passwordField = document.createElement('label');
passwordField.className = 'stack';
const passwordInput = document.createElement('input');
passwordInput.className = 'input';
passwordInput.type = 'password';
passwordInput.name = 'shine-login-password';
passwordInput.autocomplete = 'current-password';
passwordInput.autocapitalize = 'off';
passwordInput.spellcheck = false;
passwordInput.maxLength = PASSWORD_MAX_LENGTH;
passwordInput.placeholder = 'Пароль';
passwordInput.value = '';
passwordField.append(passwordInput);
const status = document.createElement('p');
status.className = 'status-line is-unavailable';
status.style.display = 'none';
const enterButton = document.createElement('button');
enterButton.className = 'primary-btn';
enterButton.type = 'button';
enterButton.textContent = 'Войти';
const actions = document.createElement('div');
actions.className = 'auth-footer-actions';
actions.append(enterButton);
const { overlay, progress } = createSecretOverlay();
const submit = async () => {
const currentLogin = String(state.loginDraft.login || '').trim();
const password = String(passwordInput.value || '');
setStatus(status, '');
if (!currentLogin) {
navigate('login-view');
return;
}
if (password.length > PASSWORD_MAX_LENGTH) {
setStatus(status, `Пароль слишком длинный. Максимальная длина: ${PASSWORD_MAX_LENGTH} символов.`);
return;
}
state.loginDraft.password = password;
state.loginDraft.passwordMode = 'single';
state.loginDraft.passwordWords = emptyPasswordWords();
setAuthBusy(true);
setAuthError('');
enterButton.disabled = true;
try {
authService.resetConnection(state.entrySettings.shineServer, { clearSessionContext: true });
// Повторная проверка защищает UI от смены access server между первым экраном и входом.
const resolved = await authService.resolveLoginForAuth(currentLogin);
if (String(resolved?.resolution || '').toUpperCase() !== 'LOCAL') {
state.loginDraft.login = String(resolved?.login || currentLogin).trim();
navigate('login-view');
return;
}
overlay.style.display = 'grid';
const keyBundle = await authService.derivePasswordKeyBundle(currentLogin, password, {
onProgress: ({ percent }) => {
const value = Math.max(0, Math.min(100, Number(percent) || 0));
progress.textContent = value > 0 && value < 100 ? `${Math.round(value)}%` : '';
},
});
overlay.style.display = 'none';
enterButton.textContent = 'Проверяем пароль…';
const result = await authService.createAuthSession(currentLogin, keyBundle);
// Существующую дальнейшую логику после успешного пароля сохраняем без изменений.
state.registrationDraft.flowType = 'login';
state.registrationDraft.login = result.login;
state.registrationDraft.password = password;
state.registrationDraft.passwordMode = 'single';
state.registrationDraft.passwordWords = emptyPasswordWords();
state.registrationDraft.sessionId = result.sessionId;
state.registrationDraft.storagePwd = result.storagePwd;
state.registrationDraft.pendingKeyBundle = keyBundle;
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
navigate('registration-keys-view');
} catch (error) {
overlay.style.display = 'none';
const message = toUserMessage(error, 'Не удалось выполнить вход.');
setAuthError(message);
setStatus(status, message);
passwordInput.select();
} finally {
setAuthBusy(false);
enterButton.disabled = false;
enterButton.textContent = 'Войти';
}
};
enterButton.addEventListener('click', submit);
passwordInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
void submit();
}
});
panel.append(title, passwordField, status, actions);
screen.append(
createTopBar({
title: '',
back: { label: '←', onClick: () => navigate('login-view') },
}),
panel,
overlay,
);
window.setTimeout(() => passwordInput.focus(), 0);
return screen;
}