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
+5 -3
View File
@@ -35,14 +35,14 @@ import {
import * as startView from './pages/start-view.js?v=202606142105';
import * as entrySettingsView from './pages/entry-settings-view.js?v=202606161240';
import * as registerView from './pages/register-view.js';
import * as registrationPaymentView from './pages/registration-payment-view.js';
import * as registrationPaymentView from './pages/registration-payment-view.js?v=202606180940';
import * as registrationKeysView from './pages/registration-keys-view.js';
import * as registrationDraftKeysView from './pages/registration-draft-keys-view.js';
import * as topupView from './pages/topup-view.js';
import * as devnetTopupView from './pages/devnet-topup-view.js';
import * as loginView from './pages/login-view.js?v=202606150110';
import * as loginCameraView from './pages/login-camera-view.js';
import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606160915';
import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606180940';
import * as loginPasswordView from './pages/login-password-view.js';
import * as keyStorageView from './pages/key-storage-view.js';
@@ -55,7 +55,8 @@ import * as serverSettingsView from './pages/server-settings-view.js?v=202606161
import * as toolsSettingsView from './pages/tools-settings-view.js';
import * as deviceView from './pages/device-view.js?v=202606131435';
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
import * as devicePairingView from './pages/device-pairing-view.js?v=202606160915';
import * as devicePairingView from './pages/device-pairing-view.js?v=202606180940';
import * as trustedDeviceLoginSettingsView from './pages/trusted-device-login-settings-view.js?v=202606180930';
import * as deviceQrView from './pages/device-qr-view.js';
import * as deviceCameraView from './pages/device-camera-view.js';
import * as showKeysView from './pages/show-keys-view.js';
@@ -100,6 +101,7 @@ const routes = {
'device-view': deviceView,
'connect-device-view': connectDeviceView,
'device-pairing-view': devicePairingView,
'trusted-device-login-settings-view': trustedDeviceLoginSettingsView,
'device-qr-view': deviceQrView,
'device-camera-view': deviceCameraView,
'show-keys-view': showKeysView,
+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) {
+3 -3
View File
@@ -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');
+28 -22
View File
@@ -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;
}
+1
View File
@@ -180,6 +180,7 @@ export function resolveToolbarActive(pageId) {
pageId === 'device-view' ||
pageId === 'connect-device-view' ||
pageId === 'device-pairing-view' ||
pageId === 'trusted-device-login-settings-view' ||
pageId === 'device-qr-view' ||
pageId === 'device-camera-view' ||
pageId === 'show-keys-view' ||
+38 -25
View File
@@ -1091,17 +1091,22 @@ export class AuthService {
if (response.status !== 200) throw opError('CloseActiveSession', response);
}
async upsertEspPairingSettings({ enabled, passwordHash = '', ttlSeconds = 180 }) {
const response = await this.ws.request('UpsertEspPairingSettings', {
enabled: !!enabled,
passwordHash: String(passwordHash || '').trim(),
ttlSeconds: Number(ttlSeconds) || 180,
});
if (response.status !== 200) throw opError('UpsertEspPairingSettings', response);
async getTrustedDeviceLoginSettings() {
const response = await this.ws.request('GetTrustedDeviceLoginSettings', {});
if (response.status !== 200) throw opError('GetTrustedDeviceLoginSettings', response);
return response.payload || {};
}
async startEspPairing({
async upsertTrustedDeviceLoginSettings({ enabled, passwordHash = '' }) {
const response = await this.ws.request('UpsertTrustedDeviceLoginSettings', {
enabled: !!enabled,
passwordHash: String(passwordHash || '').trim(),
});
if (response.status !== 200) throw opError('UpsertTrustedDeviceLoginSettings', response);
return response.payload || {};
}
async startTrustedDeviceLogin({
login,
passwordHash,
requesterSessionKey,
@@ -1109,7 +1114,7 @@ export class AuthService {
requesterClientPlatform = makeClientPlatform(),
payloadType = 3,
}) {
const response = await this.ws.request('StartEspPairing', {
const response = await this.ws.request('StartTrustedDeviceLogin', {
login: String(login || '').trim(),
passwordHash: String(passwordHash || '').trim(),
requesterSessionKey: String(requesterSessionKey || '').trim(),
@@ -1117,51 +1122,59 @@ export class AuthService {
requesterClientPlatform: String(requesterClientPlatform || '').trim() || makeClientPlatform(),
payloadType: Number(payloadType) || 3,
});
if (response.status !== 200) throw opError('StartEspPairing', response);
if (response.status !== 200) throw opError('StartTrustedDeviceLogin', response);
return response.payload || {};
}
async listEspPairingRequests() {
const response = await this.ws.request('ListEspPairingRequests', {});
if (response.status !== 200) throw opError('ListEspPairingRequests', response);
async listTrustedDeviceLoginRequests() {
const response = await this.ws.request('ListTrustedDeviceLoginRequests', {});
if (response.status !== 200) throw opError('ListTrustedDeviceLoginRequests', response);
return Array.isArray(response?.payload?.requests) ? response.payload.requests : [];
}
async approveEspPairing(pairingId, encryptedPayload) {
const response = await this.ws.request('ApproveEspPairing', {
async approveTrustedDeviceLogin(pairingId, encryptedPayload) {
const response = await this.ws.request('ApproveTrustedDeviceLogin', {
pairingId: String(pairingId || '').trim(),
encryptedPayload: String(encryptedPayload || '').trim(),
});
if (response.status !== 200) throw opError('ApproveEspPairing', response);
if (response.status !== 200) throw opError('ApproveTrustedDeviceLogin', response);
return response.payload || {};
}
async rejectEspPairing(pairingId, reason = '') {
const response = await this.ws.request('RejectEspPairing', {
async rejectTrustedDeviceLogin(pairingId, reason = '') {
const response = await this.ws.request('RejectTrustedDeviceLogin', {
pairingId: String(pairingId || '').trim(),
reason: String(reason || '').trim(),
});
if (response.status !== 200) throw opError('RejectEspPairing', response);
if (response.status !== 200) throw opError('RejectTrustedDeviceLogin', response);
return response.payload || {};
}
async cancelEspPairing(pairingId, requesterSessionKey) {
const response = await this.ws.request('CancelEspPairing', {
async cancelTrustedDeviceLogin(pairingId, requesterSessionKey) {
const response = await this.ws.request('CancelTrustedDeviceLogin', {
pairingId: String(pairingId || '').trim(),
requesterSessionKey: String(requesterSessionKey || '').trim(),
});
if (response.status !== 200) throw opError('CancelEspPairing', response);
if (response.status !== 200) throw opError('CancelTrustedDeviceLogin', response);
return response.payload || {};
}
async getEspPairingStatus(pairingId) {
const response = await this.ws.request('GetEspPairingStatus', {
async getTrustedDeviceLoginStatus(pairingId) {
const response = await this.ws.request('GetTrustedDeviceLoginStatus', {
pairingId: String(pairingId || '').trim(),
});
if (response.status !== 200) throw opError('GetEspPairingStatus', response);
if (response.status !== 200) throw opError('GetTrustedDeviceLoginStatus', response);
return response.payload || {};
}
async upsertEspPairingSettings(args) { return this.upsertTrustedDeviceLoginSettings(args); }
async startEspPairing(args) { return this.startTrustedDeviceLogin(args); }
async listEspPairingRequests() { return this.listTrustedDeviceLoginRequests(); }
async approveEspPairing(pairingId, encryptedPayload) { return this.approveTrustedDeviceLogin(pairingId, encryptedPayload); }
async rejectEspPairing(pairingId, reason = '') { return this.rejectTrustedDeviceLogin(pairingId, reason); }
async cancelEspPairing(pairingId, requesterSessionKey) { return this.cancelTrustedDeviceLogin(pairingId, requesterSessionKey); }
async getEspPairingStatus(pairingId) { return this.getTrustedDeviceLoginStatus(pairingId); }
async listSubscriptionsFeed(login, limit = 200) {
const response = await this.ws.request('ListSubscriptionsFeed', { login, limit });
if (response.status !== 200) throw opError('ListSubscriptionsFeed', response);
@@ -811,13 +811,13 @@ async function createShineUserPdaOnSolana({
};
}
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint }) {
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers = [] }) {
return createShineUserPdaOnSolana({
login,
keyBundle,
solanaEndpoint,
isServer: false,
accessServers: ['shineup.me'],
accessServers: Array.isArray(accessServers) ? accessServers : [],
});
}
@@ -138,6 +138,6 @@ export async function checkLoginExistsOnSolana({ login, solanaEndpoint }) {
return { exists: !!ai, userPda: userPda.toBase58() };
}
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint }) {
return registerUserOnSolanaShared({ login, keyBundle, solanaEndpoint });
export async function registerUserOnSolana({ login, keyBundle, solanaEndpoint, accessServers }) {
return registerUserOnSolanaShared({ login, keyBundle, solanaEndpoint, accessServers });
}
+4
View File
@@ -54,6 +54,10 @@ export function toUserMessage(error, fallback = 'Действие не выпо
return 'К сожалению сейчас нет ни одного активного устройства этого пользователя, подключенного к этому серверу в сети, и поэтому вход таким образом выполнить невозможно.';
}
if (code === 'PAIRING_NOT_AVAILABLE') {
return 'Для этого логина ещё не включено подключение по коду. На доверенном устройстве откройте «Подключить по коду» и нажмите «Включить подключение по коду» или задайте дополнительный пароль.';
}
if (code === 'PAIRING_PASSWORD_INVALID') {
return 'Пароль подключения не подходит.';
}