Улучшен вход пользователя в аккаунт

This commit is contained in:
AidarKC
2026-08-28 16:51:14 +04:00
parent f2fdb8195c
commit 357a05f7ec
25 changed files with 821 additions and 270 deletions
+64 -55
View File
@@ -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;
}
+108 -187
View File
@@ -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
View File
@@ -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;
}
+12
View File
@@ -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,
};
}
+8
View File
@@ -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 'К сожалению сейчас нет ни одного активного устройства этого пользователя, подключенного к этому серверу в сети, и поэтому вход таким образом выполнить невозможно.';
}