SHA256
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:
@@ -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) {
|
||||
|
||||
@@ -231,7 +231,7 @@ export function render({ navigate }) {
|
||||
if (!activePairingId || isDisposed) return;
|
||||
pollTimer = window.setTimeout(async () => {
|
||||
try {
|
||||
const payload = await authService.getEspPairingStatus(activePairingId);
|
||||
const payload = await authService.getTrustedDeviceLoginStatus(activePairingId);
|
||||
const stateValue = String(payload?.state || '');
|
||||
if (stateValue === 'created') {
|
||||
schedulePoll();
|
||||
@@ -315,7 +315,7 @@ export function render({ navigate }) {
|
||||
const passwordHash = usePassword
|
||||
? await deriveEspPairingPasswordHash(login, password)
|
||||
: '';
|
||||
const payload = await authService.startEspPairing({
|
||||
const payload = await authService.startTrustedDeviceLogin({
|
||||
login,
|
||||
passwordHash,
|
||||
requesterSessionKey: requesterMaterial.sessionKey,
|
||||
@@ -357,7 +357,7 @@ export function render({ navigate }) {
|
||||
}
|
||||
cancelBtn.disabled = true;
|
||||
try {
|
||||
await authService.cancelEspPairing(activePairingId, requesterMaterial.sessionKey);
|
||||
await authService.cancelTrustedDeviceLogin(activePairingId, requesterMaterial.sessionKey);
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
setStatus(status, 'Ожидание подключения отменено.', 'info');
|
||||
|
||||
@@ -194,6 +194,7 @@ export function render({ navigate }) {
|
||||
login: state.registrationDraft.login,
|
||||
keyBundle,
|
||||
solanaEndpoint: state.entrySettings.solanaServer,
|
||||
accessServers: [state.entrySettings.shineServerLogin || 'shineupme'],
|
||||
});
|
||||
} catch (solanaError) {
|
||||
const solanaMsg = formatSolanaErrorDetails(solanaError);
|
||||
@@ -307,28 +308,33 @@ function renderSolanaDoneStage({ navigate, status, keyBundle }) {
|
||||
tryLoginBtn.addEventListener('click', async () => {
|
||||
if (!canTryLogin) return;
|
||||
status.style.display = 'none';
|
||||
tryLoginBtn.disabled = true;
|
||||
tryLoginBtn.textContent = 'Вход...';
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const result = await authService.createSessionForExistingUser(
|
||||
state.registrationDraft.login,
|
||||
state.registrationDraft.password,
|
||||
);
|
||||
state.registrationDraft.flowType = 'registration';
|
||||
state.registrationDraft.sessionId = result.sessionId;
|
||||
state.registrationDraft.storagePwd = result.storagePwd;
|
||||
state.registrationDraft.pendingKeyBundle = keyBundle;
|
||||
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
|
||||
setAuthInfo(`Регистрация завершена. Вы вошли как @${result.login}. Далее откройте вкладку «Каналы».`);
|
||||
navigate('registration-keys-view');
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = toUserMessage(error, 'Пока не удалось войти. Попробуйте ещё раз через несколько секунд.');
|
||||
status.style.display = '';
|
||||
tryLoginBtn.disabled = false;
|
||||
tryLoginBtn.textContent = 'Попробовать войти на сервер';
|
||||
}
|
||||
tryLoginBtn.disabled = true;
|
||||
tryLoginBtn.textContent = 'Вход...';
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
const result = await authService.createSessionForExistingUser(
|
||||
state.registrationDraft.login,
|
||||
state.registrationDraft.password,
|
||||
);
|
||||
await authService.persistSessionMaterial(
|
||||
result.login,
|
||||
result.sessionMaterial,
|
||||
);
|
||||
const resumed = await authService.resumeSession(result.login, result.sessionId);
|
||||
state.registrationDraft.flowType = 'registration';
|
||||
state.registrationDraft.sessionId = resumed.sessionId || result.sessionId;
|
||||
state.registrationDraft.storagePwd = resumed.storagePwd || result.storagePwd;
|
||||
state.registrationDraft.pendingKeyBundle = keyBundle;
|
||||
state.registrationDraft.pendingSessionMaterial = result.sessionMaterial;
|
||||
setAuthInfo(`Регистрация завершена. Вы вошли как @${result.login}. Далее откройте вкладку «Каналы».`);
|
||||
navigate('registration-keys-view');
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = toUserMessage(error, 'Пока не удалось войти. Попробуйте ещё раз через несколько секунд.');
|
||||
status.style.display = '';
|
||||
tryLoginBtn.disabled = false;
|
||||
tryLoginBtn.textContent = 'Попробовать войти на сервер';
|
||||
}
|
||||
});
|
||||
|
||||
card.innerHTML = '';
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user