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 всё ещё не реализован
This commit is contained in:
AidarKC
2026-06-18 14:19:31 +04:00
parent cf2152dcfc
commit 56db6d0add
29 changed files with 689 additions and 241 deletions
+42 -59
View File
@@ -172,8 +172,9 @@ export function render({ navigate }) {
let requests = [];
let cleanupEvent = () => {};
let disposed = false;
let trustedDeviceLoginSettings = { enabled: true, hasPassword: false };
let settingsBusy = false;
let pairingPasswordConfigured = loadLocalPairingPasswordState(state.session.login, state.entrySettings.shineServerLogin || state.entrySettings.shineServerHttp);
let pairingPasswordConfigured = false;
let dialogMode = '';
screen.append(
@@ -350,15 +351,19 @@ export function render({ navigate }) {
passwordDialog.hidden = true;
};
const removeAdditionalPassword = async () => {
const payload = await runPairingOpWithSessionRestore(() => authService.upsertEspPairingSettings({
const enablePairingWithoutPassword = async () => {
const payload = await runPairingOpWithSessionRestore(() => authService.upsertTrustedDeviceLoginSettings({
enabled: true,
passwordHash: '',
ttlSeconds: 180,
}));
pairingPasswordConfigured = false;
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServerLogin || state.entrySettings.shineServerHttp, false);
setAuthInfo(`Подключение по коду без дополнительного пароля включено. TTL: ${payload?.ttlSeconds || 180} сек.`);
return payload;
};
const removeAdditionalPassword = async () => {
await enablePairingWithoutPassword();
setAuthInfo('Подключение по коду без дополнительного пароля включено.');
setStatus(status, 'Дополнительный пароль убран. Подключение по коду теперь работает без него.', 'info');
};
@@ -367,65 +372,42 @@ export function render({ navigate }) {
renderSettingsCard();
};
const formatTrustedDeviceLoginState = () => {
if (!trustedDeviceLoginSettings.enabled) return 'Вход через другое устройство запрещён.';
if (trustedDeviceLoginSettings.hasPassword) return 'Вход через другое устройство разрешён только с дополнительным паролем.';
return 'Вход через другое устройство разрешён без дополнительного пароля.';
};
const reloadTrustedDeviceLoginSettings = async () => {
trustedDeviceLoginSettings = await runPairingOpWithSessionRestore(() => authService.getTrustedDeviceLoginSettings());
pairingPasswordConfigured = !!trustedDeviceLoginSettings.hasPassword;
};
const renderSettingsCard = () => {
settingsCard.innerHTML = '';
const title = document.createElement('p');
title.className = 'field-label';
title.textContent = 'Дополнительный пароль';
title.textContent = 'Статус входа через другое устройство';
const stateText = document.createElement('p');
stateText.className = 'meta-muted';
stateText.textContent = pairingPasswordConfigured
? 'Установлен дополнительный пароль для подключения через другое устройство.'
: 'Дополнительный пароль для подключения через другое устройство не задан.';
stateText.textContent = formatTrustedDeviceLoginState();
const note = document.createElement('p');
note.className = 'meta-muted';
note.textContent = pairingPasswordConfigured
? 'Этот пароль не даёт права на вход сам по себе. Он только отсекает лишние заявки до того, как пользователь подтвердит подключение на доверённом устройстве.'
: 'Сейчас подключение работает без дополнительного пароля. Обычно этого достаточно. Если хотите, можно добавить простой пароль только как защиту от лишних заявок.';
note.textContent = 'Открыть подробные настройки можно на отдельном экране.';
const actions = document.createElement('div');
actions.className = 'row';
actions.style.flexWrap = 'wrap';
if (pairingPasswordConfigured) {
const changeBtn = document.createElement('button');
changeBtn.className = 'primary-btn';
changeBtn.type = 'button';
changeBtn.textContent = 'Изменить пароль';
changeBtn.disabled = settingsBusy;
changeBtn.addEventListener('click', () => openPasswordDialog('change'));
const removeBtn = document.createElement('button');
removeBtn.className = 'ghost-btn';
removeBtn.type = 'button';
removeBtn.textContent = 'Убрать пароль';
removeBtn.disabled = settingsBusy;
removeBtn.addEventListener('click', async () => {
setSettingsBusy(true);
try {
await removeAdditionalPassword();
} catch (error) {
const message = toUserMessage(error, 'Не удалось убрать дополнительный пароль.');
setAuthError(message);
setStatus(status, message, 'error');
} finally {
setSettingsBusy(false);
}
});
actions.append(changeBtn, removeBtn);
} else {
const setBtn = document.createElement('button');
setBtn.className = 'primary-btn';
setBtn.type = 'button';
setBtn.textContent = 'Задать дополнительный пароль';
setBtn.disabled = settingsBusy;
setBtn.addEventListener('click', () => openPasswordDialog('set'));
actions.append(setBtn);
}
const openSettingsBtn = document.createElement('button');
openSettingsBtn.className = 'primary-btn';
openSettingsBtn.type = 'button';
openSettingsBtn.textContent = 'Изменить настройки входа';
openSettingsBtn.disabled = settingsBusy;
openSettingsBtn.addEventListener('click', () => navigate('trusted-device-login-settings-view'));
actions.append(openSettingsBtn);
settingsCard.append(title, stateText, note, actions);
};
@@ -467,13 +449,15 @@ export function render({ navigate }) {
const reloadRequests = async ({ silent = false } = {}) => {
try {
requests = await runPairingOpWithSessionRestore(() => authService.listEspPairingRequests());
await reloadTrustedDeviceLoginSettings();
renderSettingsCard();
requests = await runPairingOpWithSessionRestore(() => authService.listTrustedDeviceLoginRequests());
renderRequests();
if (!silent) {
setStatus(status, 'Список pairing-заявок обновлён.', 'info');
setStatus(status, 'Список заявок на вход обновлён.', 'info');
}
} catch (error) {
const message = toUserMessage(error, 'Не удалось загрузить pairing-заявки.');
const message = toUserMessage(error, 'Не удалось загрузить заявки на вход.');
setAuthError(message);
setStatus(status, message, 'error');
}
@@ -508,7 +492,7 @@ export function render({ navigate }) {
});
}
const encryptedPayload = await encryptPairingPayloadForRequester(request?.requesterSessionKey, payload);
await runPairingOpWithSessionRestore(() => authService.approveEspPairing(request?.pairingId, encryptedPayload));
await runPairingOpWithSessionRestore(() => authService.approveTrustedDeviceLogin(request?.pairingId, encryptedPayload));
const sessionOnly = !withExtras && Number(request?.requesterSessionType || 0) === 50;
showToast(
withExtras
@@ -592,16 +576,15 @@ export function render({ navigate }) {
dialogSaveBtn.disabled = true;
try {
const passwordHash = await deriveEspPairingPasswordHash(state.session.login, password);
const payload = await runPairingOpWithSessionRestore(() => authService.upsertEspPairingSettings({
await runPairingOpWithSessionRestore(() => authService.upsertTrustedDeviceLoginSettings({
enabled: true,
passwordHash,
ttlSeconds: 180,
}));
pairingPasswordConfigured = true;
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServerLogin || state.entrySettings.shineServerHttp, true);
closePasswordDialog();
renderSettingsCard();
setAuthInfo(`Подключение по коду включено с дополнительным паролем. TTL: ${payload?.ttlSeconds || 180} сек.`);
setAuthInfo('Подключение по коду включено с дополнительным паролем.');
setStatus(status, currentMode === 'change'
? 'Дополнительный пароль изменён.'
: 'Дополнительный пароль задан.', 'info');
@@ -641,7 +624,7 @@ export function render({ navigate }) {
} else if (action === 'approve-full') {
await approveRequest(request, 'with-extras');
} else if (action === 'reject') {
await runPairingOpWithSessionRestore(() => authService.rejectEspPairing(pairingId, 'rejected_by_user'));
await runPairingOpWithSessionRestore(() => authService.rejectTrustedDeviceLogin(pairingId, 'rejected_by_user'));
showToast('Заявка отклонена', { kind: 'error' });
await reloadRequests({ silent: true });
}
@@ -658,9 +641,9 @@ export function render({ navigate }) {
renderSettingsCard();
await loadSavedKeys();
await reloadRequests({ silent: true });
cleanupEvent = authService.onEvent('IncomingEspPairingRequest', () => {
cleanupEvent = authService.onEvent('IncomingTrustedDeviceLoginRequest', () => {
if (disposed) return;
showToast('Пришла новая заявка на подключение устройства');
showToast('Пришла новая заявка на вход через доверенное устройство');
void reloadRequests({ silent: true });
});
} catch (error) {