Что сделано:\n- публичный API сценария входа через доверенное устройство переведён на TrustedDeviceLogin\n- добавлен GetTrustedDeviceLoginSettings\n- отсутствие записи настроек на сервере теперь трактуется как enabled=true и hasPassword=false\n- ttlSeconds убран из клиентского API, TTL заявки фиксирован на сервере: 300 секунд\n- в shine-UI добавлен отдельный экран настроек входа через устройство и статус на основном экране\n- browser wallet переведён на новые TrustedDeviceLogin операции\n- в wallet добавлен выбор rootKey/deviceKey для будущего запроса подписи\n- документация API обновлена\n\nЧто ещё не проверено вручную end-to-end:\n- полный сценарий UI/plugin после этого деплоя не прогонялся руками до конца\n- сам signaling подписи в wallet всё ещё не реализован
201 lines
8.0 KiB
JavaScript
201 lines
8.0 KiB
JavaScript
import { renderHeader } from '../components/header.js';
|
||
import { authService, setAuthError, setAuthInfo, state } from '../state.js';
|
||
import { deriveEspPairingPasswordHash } from '../services/device-pairing-service.js';
|
||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||
|
||
export const pageMeta = { id: 'trusted-device-login-settings-view', title: 'Настройки входа через устройство' };
|
||
|
||
function setStatus(statusEl, message, kind = 'info') {
|
||
statusEl.classList.toggle('is-unavailable', kind === 'error');
|
||
statusEl.classList.toggle('is-available', kind !== 'error');
|
||
statusEl.textContent = message;
|
||
statusEl.style.display = message ? '' : 'none';
|
||
}
|
||
|
||
function describeState(settings) {
|
||
if (!settings?.enabled) return 'Вход через другое устройство запрещён.';
|
||
if (settings?.hasPassword) return 'Вход через другое устройство разрешён только с дополнительным паролем.';
|
||
return 'Вход через другое устройство разрешён без дополнительного пароля.';
|
||
}
|
||
|
||
export function render({ navigate }) {
|
||
const screen = document.createElement('section');
|
||
screen.className = 'stack';
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'card stack';
|
||
|
||
const summary = document.createElement('p');
|
||
summary.className = 'auth-copy';
|
||
summary.textContent = 'Загружаем текущие настройки...';
|
||
|
||
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';
|
||
|
||
const actions = document.createElement('div');
|
||
actions.className = 'row';
|
||
actions.style.flexWrap = 'wrap';
|
||
|
||
const enableToggleBtn = document.createElement('button');
|
||
enableToggleBtn.className = 'primary-btn';
|
||
enableToggleBtn.type = 'button';
|
||
|
||
const noPasswordBtn = document.createElement('button');
|
||
noPasswordBtn.className = 'ghost-btn';
|
||
noPasswordBtn.type = 'button';
|
||
noPasswordBtn.textContent = 'Сделать вход без пароля';
|
||
|
||
const passwordForm = document.createElement('div');
|
||
passwordForm.className = 'stack';
|
||
passwordForm.innerHTML = `
|
||
<label class="stack">
|
||
<span class="field-label">Новый дополнительный пароль</span>
|
||
<input class="input" id="trusted-login-password" type="password" autocomplete="new-password" placeholder="Введите пароль" />
|
||
</label>
|
||
<label class="stack">
|
||
<span class="field-label">Подтверждение пароля</span>
|
||
<input class="input" id="trusted-login-password-confirm" type="password" autocomplete="new-password" placeholder="Повторите пароль" />
|
||
</label>
|
||
<button class="primary-btn" type="button" id="trusted-login-password-save">Сохранить новый пароль</button>
|
||
`;
|
||
|
||
const passwordInput = passwordForm.querySelector('#trusted-login-password');
|
||
const passwordConfirmInput = passwordForm.querySelector('#trusted-login-password-confirm');
|
||
const savePasswordBtn = passwordForm.querySelector('#trusted-login-password-save');
|
||
|
||
card.append(summary, hint, actions, passwordForm, status);
|
||
|
||
let settings = { enabled: true, hasPassword: false };
|
||
let busy = false;
|
||
|
||
const setBusy = (flag) => {
|
||
busy = flag;
|
||
enableToggleBtn.disabled = flag;
|
||
noPasswordBtn.disabled = flag || !settings.enabled || !settings.hasPassword;
|
||
savePasswordBtn.disabled = flag;
|
||
passwordInput.disabled = flag;
|
||
passwordConfirmInput.disabled = flag;
|
||
};
|
||
|
||
const renderUi = () => {
|
||
summary.textContent = describeState(settings);
|
||
enableToggleBtn.textContent = settings.enabled
|
||
? 'Запретить вход через другое устройство'
|
||
: 'Разрешить вход через другое устройство';
|
||
|
||
actions.innerHTML = '';
|
||
actions.append(enableToggleBtn);
|
||
if (settings.enabled) {
|
||
actions.append(noPasswordBtn);
|
||
}
|
||
passwordForm.style.display = settings.enabled ? '' : 'none';
|
||
noPasswordBtn.disabled = busy || !settings.hasPassword;
|
||
setBusy(busy);
|
||
};
|
||
|
||
const reloadSettings = async () => {
|
||
settings = await authService.getTrustedDeviceLoginSettings();
|
||
renderUi();
|
||
};
|
||
|
||
enableToggleBtn.addEventListener('click', async () => {
|
||
setStatus(status, '', 'info');
|
||
setBusy(true);
|
||
try {
|
||
settings = await authService.upsertTrustedDeviceLoginSettings({
|
||
enabled: !settings.enabled,
|
||
passwordHash: '',
|
||
});
|
||
renderUi();
|
||
setAuthInfo(settings.enabled
|
||
? 'Вход через другое устройство разрешён.'
|
||
: 'Вход через другое устройство запрещён.');
|
||
setStatus(status, describeState(settings), 'info');
|
||
} catch (error) {
|
||
const message = toUserMessage(error, 'Не удалось изменить режим входа.');
|
||
setAuthError(message);
|
||
setStatus(status, message, 'error');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
});
|
||
|
||
noPasswordBtn.addEventListener('click', async () => {
|
||
setStatus(status, '', 'info');
|
||
setBusy(true);
|
||
try {
|
||
settings = await authService.upsertTrustedDeviceLoginSettings({
|
||
enabled: true,
|
||
passwordHash: '',
|
||
});
|
||
renderUi();
|
||
setAuthInfo('Вход через другое устройство теперь работает без дополнительного пароля.');
|
||
setStatus(status, 'Вход теперь работает без дополнительного пароля.', 'info');
|
||
} catch (error) {
|
||
const message = toUserMessage(error, 'Не удалось убрать дополнительный пароль.');
|
||
setAuthError(message);
|
||
setStatus(status, message, 'error');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
});
|
||
|
||
savePasswordBtn.addEventListener('click', async () => {
|
||
setStatus(status, '', 'info');
|
||
const password = String(passwordInput.value || '');
|
||
const confirm = String(passwordConfirmInput.value || '');
|
||
if (!password || !confirm) {
|
||
setStatus(status, 'Заполните пароль и подтверждение.', 'error');
|
||
return;
|
||
}
|
||
if (password !== confirm) {
|
||
setStatus(status, 'Пароли не совпадают.', 'error');
|
||
return;
|
||
}
|
||
|
||
setBusy(true);
|
||
try {
|
||
const finalHash = await deriveEspPairingPasswordHash(
|
||
String(state.session.login || ''),
|
||
password,
|
||
);
|
||
settings = await authService.upsertTrustedDeviceLoginSettings({
|
||
enabled: true,
|
||
passwordHash: finalHash,
|
||
});
|
||
passwordInput.value = '';
|
||
passwordConfirmInput.value = '';
|
||
renderUi();
|
||
setAuthInfo('Дополнительный пароль для входа через другое устройство сохранён.');
|
||
setStatus(status, 'Дополнительный пароль сохранён.', 'info');
|
||
} catch (error) {
|
||
const message = toUserMessage(error, 'Не удалось сохранить дополнительный пароль.');
|
||
setAuthError(message);
|
||
setStatus(status, message, 'error');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
});
|
||
|
||
screen.append(
|
||
renderHeader({
|
||
title: 'Настройки входа через устройство',
|
||
leftAction: { label: '←', onClick: () => navigate('device-pairing-view') },
|
||
}),
|
||
card,
|
||
);
|
||
|
||
void reloadSettings().catch((error) => {
|
||
const message = toUserMessage(error, 'Не удалось загрузить настройки входа через устройство.');
|
||
setAuthError(message);
|
||
setStatus(status, message, 'error');
|
||
});
|
||
|
||
return screen;
|
||
}
|