SHiNE-server/shine-UI/js/pages/trusted-device-login-settings-view.js
AidarKC 56db6d0add TrustedDeviceLogin API и настройки входа через устройство
Что сделано:\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 всё ещё не реализован
2026-06-18 14:19:31 +04:00

201 lines
8.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}