SHA256
Улучшен вход пользователя в аккаунт
This commit is contained in:
@@ -73,7 +73,7 @@ export function render({ navigate }) {
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Войти через другое устройство',
|
||||
leftAction: { label: '←', onClick: () => navigate('login-view') },
|
||||
leftAction: { label: '←', onClick: () => { void cancelActivePairingAndBack(); } },
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -82,22 +82,17 @@ export function render({ navigate }) {
|
||||
panel.innerHTML = '<h1 class="login-panel-title">Войти через другое устройство</h1>';
|
||||
|
||||
const formCard = document.createElement('div');
|
||||
formCard.className = 'card stack';
|
||||
formCard.className = 'card stack login-device-preparation';
|
||||
formCard.innerHTML = `
|
||||
<label class="stack">
|
||||
<span class="field-label">Введите логин</span>
|
||||
<input class="input" id="pair-login" type="text" autocomplete="username" placeholder="" value="" />
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" id="pair-use-password" />
|
||||
использовать доп. пароль
|
||||
</label>
|
||||
<label class="stack">
|
||||
<span class="field-label">Пароль подключения</span>
|
||||
<p class="auth-copy" id="pair-login-label"></p>
|
||||
<input id="pair-login" type="hidden" value="" />
|
||||
<input id="pair-use-password" type="checkbox" hidden />
|
||||
<label class="stack" id="pair-password-wrap" style="display:none;">
|
||||
<span class="field-label">Дополнительный пароль подключения</span>
|
||||
<input class="input" id="pair-password" type="password" autocomplete="current-password" placeholder="Пароль, заданный на другом устройстве" />
|
||||
</label>
|
||||
<button class="primary-btn" type="button" id="pair-start-btn">Получить код</button>
|
||||
<p class="meta-muted" id="pair-mode-hint">Получить код для входа можно только если сейчас есть хотя бы одно другое активное устройство этого пользователя в сети.</p>
|
||||
<button class="primary-btn" type="button" id="pair-start-btn" style="display:none;">Получить код</button>
|
||||
<p class="meta-muted" id="pair-mode-hint">Создаём код для входа…</p>
|
||||
`;
|
||||
|
||||
const status = document.createElement('p');
|
||||
@@ -110,10 +105,14 @@ export function render({ navigate }) {
|
||||
resultWrap.innerHTML = codeCardHtml();
|
||||
|
||||
const loginInput = formCard.querySelector('#pair-login');
|
||||
const loginLabelEl = formCard.querySelector('#pair-login-label');
|
||||
const usePasswordInput = formCard.querySelector('#pair-use-password');
|
||||
const passwordInput = formCard.querySelector('#pair-password');
|
||||
const startBtn = formCard.querySelector('#pair-start-btn');
|
||||
const modeHintEl = formCard.querySelector('#pair-mode-hint');
|
||||
|
||||
loginInput.value = String(state.loginDraft.login || '').trim();
|
||||
loginLabelEl.textContent = loginInput.value ? `Вход для @${loginInput.value}` : '';
|
||||
const shortCodeEl = resultWrap.querySelector('#pairing-short-code');
|
||||
const statusHintEl = resultWrap.querySelector('#pairing-status-hint');
|
||||
const onlineHintEl = resultWrap.querySelector('#pairing-online-hint');
|
||||
@@ -127,12 +126,11 @@ export function render({ navigate }) {
|
||||
const syncPasswordUi = () => {
|
||||
const usePassword = !!usePasswordInput.checked;
|
||||
passwordInput.parentElement.style.display = usePassword ? '' : 'none';
|
||||
startBtn.style.display = usePassword ? '' : 'none';
|
||||
modeHintEl.textContent = usePassword
|
||||
? 'Получить код для входа можно только если сейчас есть хотя бы одно другое активное устройство этого пользователя в сети. Если на доверённом устройстве включён доп. пароль, введите его.'
|
||||
: 'Получить код для входа можно только если сейчас есть хотя бы одно другое активное устройство этого пользователя в сети.';
|
||||
if (!usePassword) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
? 'На доверённом устройстве для входа по коду включён дополнительный пароль.'
|
||||
: 'Код можно подтвердить на уже подключённом устройстве. Если оно сейчас не в сети, заявка будет ждать до истечения срока.';
|
||||
if (!usePassword) passwordInput.value = '';
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
@@ -157,6 +155,8 @@ export function render({ navigate }) {
|
||||
activePairingId = '';
|
||||
activeExpiresAtMs = 0;
|
||||
startBtn.disabled = false;
|
||||
startBtn.style.display = '';
|
||||
startBtn.textContent = 'Получить новый код';
|
||||
cancelBtn.style.display = 'none';
|
||||
resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl);
|
||||
setStatus(status, 'Время ожидания истекло. Получите новый код.', 'error');
|
||||
@@ -310,14 +310,16 @@ export function render({ navigate }) {
|
||||
setAuthBusy(true);
|
||||
setAuthError('');
|
||||
setAuthInfo('');
|
||||
setStatus(status, 'Проверяем пользователя и создаём pairing-заявку...', 'info');
|
||||
setStatus(status, 'Создаём код для входа…', 'info');
|
||||
clearActivePairing();
|
||||
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const user = await authService.getUser(login);
|
||||
if (!user?.exists) {
|
||||
throw new Error('Пользователь не найден.');
|
||||
const resolved = await authService.resolveLoginForAuth(login);
|
||||
if (String(resolved?.resolution || '').toUpperCase() !== 'LOCAL') {
|
||||
state.loginDraft.login = String(resolved?.login || login).trim();
|
||||
navigate('login-view');
|
||||
return;
|
||||
}
|
||||
|
||||
requesterMaterial = await createRequesterPairingMaterial();
|
||||
@@ -339,59 +341,66 @@ export function render({ navigate }) {
|
||||
shortCodeEl.textContent = formatPairingShortCode(payload?.shortCode || '');
|
||||
statusHintEl.textContent = 'Откройте на доверенном устройстве: Настройки -> Устройства -> Подключить устройство -> Подключить по коду.';
|
||||
onlineHintEl.textContent = payload?.trustedSessionOnline
|
||||
? 'Сейчас есть хотя бы одна онлайн доверенная сессия, которая может принять заявку.'
|
||||
: 'Сейчас нет онлайн доверенной сессии. Заявка будет ждать, пока пользователь откроет уже подключённое устройство.';
|
||||
? 'Доверенное устройство сейчас в сети и может сразу принять заявку.'
|
||||
: 'Доверенное устройство сейчас не в сети. Заявка будет ждать его подключения.';
|
||||
resultWrap.style.display = '';
|
||||
cancelBtn.style.display = '';
|
||||
startCountdown(payload?.expiresAtMs);
|
||||
state.loginDraft.login = login;
|
||||
setStatus(status, 'Код создан. Ожидаем подтверждение на другом устройстве...', 'info');
|
||||
schedulePoll();
|
||||
} catch (error) {
|
||||
startBtn.disabled = false;
|
||||
const message = toUserMessage(error, 'Не удалось начать вход через другое устройство.');
|
||||
setAuthError(message);
|
||||
setStatus(status, message, 'error');
|
||||
if (String(error?.code || '').toUpperCase() === 'PAIRING_PASSWORD_INVALID' && !usePassword) {
|
||||
usePasswordInput.checked = true;
|
||||
syncPasswordUi();
|
||||
modeHintEl.textContent = 'Для этого аккаунта включён дополнительный пароль подключения. Введите его, чтобы получить код.';
|
||||
setStatus(status, 'Введите дополнительный пароль подключения.', 'info');
|
||||
window.setTimeout(() => passwordInput.focus(), 0);
|
||||
} else {
|
||||
startBtn.style.display = '';
|
||||
startBtn.textContent = 'Повторить';
|
||||
const message = toUserMessage(error, 'Не удалось начать вход через другое устройство.');
|
||||
setAuthError(message);
|
||||
setStatus(status, message, 'error');
|
||||
}
|
||||
} finally {
|
||||
setAuthBusy(false);
|
||||
}
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', async () => {
|
||||
if (!activePairingId || !requesterMaterial?.sessionKey) {
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
cancelBtn.style.display = 'none';
|
||||
return;
|
||||
const cancelActivePairingAndBack = async () => {
|
||||
const pairingId = activePairingId;
|
||||
const requesterSessionKey = requesterMaterial?.sessionKey;
|
||||
isDisposed = true;
|
||||
stopPolling();
|
||||
stopCountdown();
|
||||
if (pairingId && requesterSessionKey) {
|
||||
try {
|
||||
await authService.cancelTrustedDeviceLogin(pairingId, requesterSessionKey);
|
||||
} catch {
|
||||
// Навигацию назад не блокируем из-за ошибки отмены уже созданной заявки.
|
||||
}
|
||||
}
|
||||
cancelBtn.disabled = true;
|
||||
try {
|
||||
await authService.cancelTrustedDeviceLogin(activePairingId, requesterMaterial.sessionKey);
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
setStatus(status, 'Ожидание подключения отменено.', 'info');
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось отменить ожидание подключения.');
|
||||
setAuthError(message);
|
||||
setStatus(status, message, 'error');
|
||||
} finally {
|
||||
cancelBtn.disabled = false;
|
||||
cancelBtn.style.display = activePairingId ? '' : 'none';
|
||||
}
|
||||
});
|
||||
navigate('login-view');
|
||||
};
|
||||
|
||||
screen.cleanup = () => {
|
||||
isDisposed = true;
|
||||
stopPolling();
|
||||
stopCountdown();
|
||||
if (activePairingId && requesterMaterial?.sessionKey) {
|
||||
void authService.cancelTrustedDeviceLogin(activePairingId, requesterMaterial.sessionKey).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const resultActions = document.createElement('div');
|
||||
resultActions.className = 'row';
|
||||
resultActions.append(cancelBtn);
|
||||
resultWrap.append(resultActions);
|
||||
|
||||
panel.append(formCard, status, resultWrap);
|
||||
screen.append(panel);
|
||||
|
||||
if (!String(loginInput.value || '').trim()) {
|
||||
window.setTimeout(() => navigate('login-view'), 0);
|
||||
} else {
|
||||
// После проверки логина на предыдущем экране код создаётся сразу.
|
||||
window.setTimeout(() => startBtn.click(), 0);
|
||||
}
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -7,256 +7,177 @@ import {
|
||||
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';
|
||||
import { emptyPasswordWords, PASSWORD_MAX_LENGTH } from '../services/password-words.js';
|
||||
|
||||
function createWordsLayout({ words, onInput }) {
|
||||
const section = document.createElement('div');
|
||||
section.className = 'registration-words-block';
|
||||
export const pageMeta = { id: 'login-password-view', title: 'Введите пароль', showAppChrome: false };
|
||||
|
||||
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 };
|
||||
function setStatus(statusEl, message) {
|
||||
statusEl.textContent = message;
|
||||
statusEl.style.display = message ? '' : 'none';
|
||||
}
|
||||
|
||||
export const pageMeta = { id: 'login-password-view', title: 'Войти по логину', showAppChrome: false };
|
||||
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';
|
||||
screen.className = 'stack auth-screen auth-screen--lower';
|
||||
|
||||
clearAuthMessages();
|
||||
|
||||
const form = document.createElement('div');
|
||||
form.className = 'card stack';
|
||||
const login = String(state.loginDraft.login || '').trim();
|
||||
if (!login) {
|
||||
window.setTimeout(() => navigate('login-view'), 0);
|
||||
}
|
||||
|
||||
let passwordMode = String(state.loginDraft.passwordMode || 'single') === 'words' ? 'words' : 'single';
|
||||
let passwordWords = normalizePasswordWords(state.loginDraft.passwordWords);
|
||||
const panel = document.createElement('section');
|
||||
panel.className = 'login-panel stack';
|
||||
|
||||
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 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 = 'new-password';
|
||||
passwordInput.autocomplete = 'current-password';
|
||||
passwordInput.autocapitalize = 'off';
|
||||
passwordInput.spellcheck = false;
|
||||
passwordInput.maxLength = PASSWORD_MAX_LENGTH;
|
||||
passwordInput.value = passwordMode === 'single' ? state.loginDraft.password : '';
|
||||
passwordInput.placeholder = 'Введите пароль';
|
||||
passwordInput.placeholder = 'Пароль';
|
||||
passwordInput.value = '';
|
||||
|
||||
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 = 'Введите логин. На следующем шаге сохраните ключи на устройстве.';
|
||||
passwordField.append(passwordInput);
|
||||
|
||||
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 = `
|
||||
<label class="stack"><span class="field-label">Логин</span></label>
|
||||
<label class="stack"><span class="field-label">Пароль</span></label>
|
||||
`;
|
||||
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 = '';
|
||||
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 (state.loginDraft.password.length > PASSWORD_MAX_LENGTH) {
|
||||
status.textContent = `Пароль слишком длинный. Максимальная длина: ${PASSWORD_MAX_LENGTH} символов.`;
|
||||
status.style.display = '';
|
||||
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;
|
||||
enterButton.textContent = 'Входим...';
|
||||
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const result = await authService.createSessionForExistingUser(state.loginDraft.login, state.loginDraft.password);
|
||||
|
||||
// Повторная проверка защищает 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 = state.loginDraft.password;
|
||||
state.registrationDraft.passwordMode = state.loginDraft.passwordMode;
|
||||
state.registrationDraft.passwordWords = normalizePasswordWords(state.loginDraft.passwordWords);
|
||||
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 = result.keyBundle;
|
||||
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);
|
||||
status.textContent = message;
|
||||
status.style.display = '';
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
actions.append(backButton, enterButton);
|
||||
panel.append(title, passwordField, status, actions);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
title: 'Войти по логину',
|
||||
leftAction: { label: '←', onClick: () => navigate('start-view') },
|
||||
title: '',
|
||||
leftAction: { label: '←', onClick: () => navigate('login-view') },
|
||||
}),
|
||||
form,
|
||||
actions,
|
||||
panel,
|
||||
overlay,
|
||||
);
|
||||
|
||||
window.setTimeout(() => passwordInput.focus(), 0);
|
||||
return screen;
|
||||
}
|
||||
|
||||
+143
-17
@@ -1,31 +1,156 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import {
|
||||
authService,
|
||||
clearAuthMessages,
|
||||
setAuthBusy,
|
||||
setAuthError,
|
||||
state,
|
||||
} from '../state.js';
|
||||
import { buildShineHttpUrlFromAddress } from '../services/shine-server-resolver.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
|
||||
export const pageMeta = { id: 'login-view', title: 'Войти', showAppChrome: false };
|
||||
|
||||
function setStatus(statusEl, message, kind = 'error') {
|
||||
statusEl.classList.toggle('is-unavailable', kind === 'error');
|
||||
statusEl.classList.toggle('is-available', kind !== 'error');
|
||||
statusEl.textContent = message;
|
||||
statusEl.style.display = message ? '' : 'none';
|
||||
}
|
||||
|
||||
function setRemoteServer(remoteWrap, serverLoginEl, serverLinkEl, payload) {
|
||||
const serverLogin = String(payload?.accessServerLogin || '').trim();
|
||||
const serverUrl = String(payload?.accessServerUrl || '').trim();
|
||||
serverLoginEl.textContent = serverLogin ? `@${serverLogin}` : 'другой сервер доступа';
|
||||
serverLinkEl.textContent = serverUrl || 'Открыть сервер';
|
||||
serverLinkEl.href = buildShineHttpUrlFromAddress(serverUrl);
|
||||
remoteWrap.style.display = '';
|
||||
}
|
||||
|
||||
export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack auth-screen auth-screen--lower login-choice-screen';
|
||||
|
||||
const loginButton = document.createElement('button');
|
||||
loginButton.className = 'ghost-btn';
|
||||
loginButton.type = 'button';
|
||||
loginButton.textContent = 'Войти по паролю';
|
||||
loginButton.addEventListener('click', () => navigate('login-password-view'));
|
||||
|
||||
const otherDeviceButton = document.createElement('button');
|
||||
otherDeviceButton.className = 'ghost-btn';
|
||||
otherDeviceButton.type = 'button';
|
||||
otherDeviceButton.textContent = 'Войти через другое устройство';
|
||||
otherDeviceButton.addEventListener('click', () => navigate('login-other-device-view'));
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-actions login-actions-wide';
|
||||
actions.append(loginButton, otherDeviceButton);
|
||||
clearAuthMessages();
|
||||
|
||||
const panel = document.createElement('section');
|
||||
panel.className = 'login-panel stack';
|
||||
panel.innerHTML = '<h1 class="login-panel-title">Войти</h1>';
|
||||
panel.append(actions);
|
||||
|
||||
const title = document.createElement('h1');
|
||||
title.className = 'login-panel-title';
|
||||
title.textContent = 'Введите логин';
|
||||
|
||||
const loginField = document.createElement('label');
|
||||
loginField.className = 'stack';
|
||||
|
||||
const loginInput = document.createElement('input');
|
||||
loginInput.className = 'input';
|
||||
loginInput.type = 'text';
|
||||
loginInput.autocomplete = 'username';
|
||||
loginInput.autocapitalize = 'off';
|
||||
loginInput.spellcheck = false;
|
||||
loginInput.placeholder = 'Логин';
|
||||
loginInput.value = String(state.loginDraft.login || '');
|
||||
|
||||
loginField.append(loginInput);
|
||||
|
||||
const status = document.createElement('p');
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.style.display = 'none';
|
||||
|
||||
const remoteWrap = document.createElement('div');
|
||||
remoteWrap.className = 'login-remote-server stack';
|
||||
remoteWrap.style.display = 'none';
|
||||
|
||||
const remoteText = document.createElement('p');
|
||||
remoteText.className = 'auth-copy';
|
||||
const serverLoginEl = document.createElement('strong');
|
||||
const textBefore = document.createTextNode('Этот пользователь SHiNE зарегистрирован на другом сервере доступа: ');
|
||||
remoteText.append(textBefore, serverLoginEl, document.createTextNode('. Для входа перейдите на его сервер.'));
|
||||
|
||||
const serverLinkEl = document.createElement('a');
|
||||
serverLinkEl.className = 'primary-btn login-server-link';
|
||||
serverLinkEl.target = '_self';
|
||||
serverLinkEl.rel = 'noopener';
|
||||
|
||||
remoteWrap.append(remoteText, serverLinkEl);
|
||||
|
||||
const passwordButton = document.createElement('button');
|
||||
passwordButton.className = 'primary-btn';
|
||||
passwordButton.type = 'button';
|
||||
passwordButton.textContent = 'Войти по паролю';
|
||||
|
||||
const deviceButton = document.createElement('button');
|
||||
deviceButton.className = 'ghost-btn';
|
||||
deviceButton.type = 'button';
|
||||
deviceButton.textContent = 'Войти через другое устройство';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'auth-actions login-actions-wide';
|
||||
actions.append(passwordButton, deviceButton);
|
||||
|
||||
const resolveAndContinue = async (targetPage) => {
|
||||
const login = String(loginInput.value || '').trim();
|
||||
setStatus(status, '');
|
||||
remoteWrap.style.display = 'none';
|
||||
if (!login) {
|
||||
setStatus(status, 'Введите логин.');
|
||||
loginInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
passwordButton.disabled = true;
|
||||
deviceButton.disabled = true;
|
||||
setAuthBusy(true);
|
||||
setAuthError('');
|
||||
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const resolved = await authService.resolveLoginForAuth(login);
|
||||
const resolution = String(resolved?.resolution || '').trim().toUpperCase();
|
||||
|
||||
if (resolution === 'NOT_FOUND') {
|
||||
setStatus(status, 'Пользователь с таким логином не зарегистрирован в SHiNE.');
|
||||
return;
|
||||
}
|
||||
if (resolution === 'NO_ACCESS_SERVER') {
|
||||
setStatus(status, 'Пользователь зарегистрирован в SHiNE, но для него не найден действующий сервер доступа.');
|
||||
return;
|
||||
}
|
||||
if (resolution === 'REMOTE') {
|
||||
state.loginDraft.login = String(resolved?.login || login).trim();
|
||||
setRemoteServer(remoteWrap, serverLoginEl, serverLinkEl, resolved);
|
||||
return;
|
||||
}
|
||||
if (resolution !== 'LOCAL') {
|
||||
setStatus(status, 'Сервер вернул неизвестный статус проверки логина.');
|
||||
return;
|
||||
}
|
||||
|
||||
state.loginDraft.login = String(resolved?.login || login).trim();
|
||||
state.loginDraft.password = '';
|
||||
navigate(targetPage);
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось проверить логин.');
|
||||
setAuthError(message);
|
||||
setStatus(status, message);
|
||||
} finally {
|
||||
setAuthBusy(false);
|
||||
passwordButton.disabled = false;
|
||||
deviceButton.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
passwordButton.addEventListener('click', () => resolveAndContinue('login-password-view'));
|
||||
deviceButton.addEventListener('click', () => resolveAndContinue('login-other-device-view'));
|
||||
loginInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void resolveAndContinue('login-password-view');
|
||||
}
|
||||
});
|
||||
|
||||
panel.append(title, loginField, status, remoteWrap, actions);
|
||||
|
||||
screen.append(
|
||||
renderHeader({
|
||||
@@ -35,5 +160,6 @@ export function render({ navigate }) {
|
||||
panel,
|
||||
);
|
||||
|
||||
window.setTimeout(() => loginInput.focus(), 0);
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -1042,6 +1042,14 @@ export class AuthService {
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async resolveLoginForAuth(login) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) throw new Error('Введите логин');
|
||||
const response = await this.ws.request('ResolveLoginForAuth', { login: cleanLogin });
|
||||
if (response.status !== 200) throw opError('ResolveLoginForAuth', response);
|
||||
return response.payload || {};
|
||||
}
|
||||
|
||||
async resolveCanonicalDisplayLogin(login) {
|
||||
const cleanLogin = String(login || '').trim();
|
||||
if (!cleanLogin) return '';
|
||||
@@ -1158,6 +1166,7 @@ export class AuthService {
|
||||
|
||||
const sessionId = createResp?.payload?.sessionId;
|
||||
if (!sessionId) throw new Error('CreateAuthSession: не вернулся sessionId');
|
||||
const connectionScope = String(createResp?.payload?.connectionScope || '').trim().toUpperCase();
|
||||
|
||||
const canonicalLogin = await this.resolveCanonicalDisplayLogin(cleanLogin);
|
||||
|
||||
@@ -1165,6 +1174,7 @@ export class AuthService {
|
||||
login: canonicalLogin,
|
||||
sessionId,
|
||||
storagePwd,
|
||||
connectionScope,
|
||||
sessionMaterial: {
|
||||
sessionId,
|
||||
sessionKey,
|
||||
@@ -1322,6 +1332,7 @@ export class AuthService {
|
||||
|
||||
const storagePwd = loginResp?.payload?.storagePwd;
|
||||
if (!storagePwd) throw new Error('SessionLogin: не вернулся storagePwd');
|
||||
const connectionScope = String(loginResp?.payload?.connectionScope || '').trim().toUpperCase();
|
||||
|
||||
const canonicalLogin = await this.resolveCanonicalDisplayLogin(cleanLogin);
|
||||
|
||||
@@ -1329,6 +1340,7 @@ export class AuthService {
|
||||
login: canonicalLogin,
|
||||
sessionId: targetSessionId,
|
||||
storagePwd,
|
||||
connectionScope,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,14 @@ export function toUserMessage(error, fallback = 'Действие не выпо
|
||||
return 'Пользователь не найден. Проверьте логин.';
|
||||
}
|
||||
|
||||
if (code === 'DEVICE_KEY_NOT_ACTUAL') {
|
||||
return 'Неверный пароль.';
|
||||
}
|
||||
|
||||
if (code === 'USER_NOT_LOCAL') {
|
||||
return 'Этот пользователь относится к другому серверу доступа.';
|
||||
}
|
||||
|
||||
if (code === 'PAIRING_NO_TRUSTED_SESSION_ONLINE') {
|
||||
return 'К сожалению сейчас нет ни одного активного устройства этого пользователя, подключенного к этому серверу в сети, и поэтому вход таким образом выполнить невозможно.';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user