Разрешить pairing без доп пароля
This commit is contained in:
parent
49fdbbf7ae
commit
bef205aec7
@ -188,6 +188,8 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Если pairing должен работать **без доп. пароля**, клиент может включить его с пустым `passwordHash`.
|
||||||
|
|
||||||
### Успешный ответ
|
### Успешный ответ
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -205,7 +207,6 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
|
|||||||
|
|
||||||
### Ошибки
|
### Ошибки
|
||||||
|
|
||||||
- `400 / EMPTY_PASSWORD_HASH` — попытка включить pairing без `passwordHash`.
|
|
||||||
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||||||
|
|
||||||
### 5.2. `StartEspPairing`
|
### 5.2. `StartEspPairing`
|
||||||
@ -229,6 +230,8 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Если на доверённом устройстве pairing включён **без доп. пароля**, новое устройство может отправить пустой `passwordHash`.
|
||||||
|
|
||||||
Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку.
|
Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку.
|
||||||
|
|
||||||
### Успешный ответ
|
### Успешный ответ
|
||||||
@ -253,7 +256,6 @@ K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M
|
|||||||
### Ошибки
|
### Ошибки
|
||||||
|
|
||||||
- `400 / EMPTY_LOGIN`
|
- `400 / EMPTY_LOGIN`
|
||||||
- `400 / EMPTY_PASSWORD_HASH`
|
|
||||||
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
||||||
- `400 / BAD_REQUESTER_SESSION_KEY`
|
- `400 / BAD_REQUESTER_SESSION_KEY`
|
||||||
- `400 / BAD_SESSION_TYPE`
|
- `400 / BAD_SESSION_TYPE`
|
||||||
|
|||||||
@ -4,11 +4,13 @@
|
|||||||
- в UI добавлен новый сценарий подключения устройства через доверенную уже авторизованную сессию пользователя;
|
- в UI добавлен новый сценарий подключения устройства через доверенную уже авторизованную сессию пользователя;
|
||||||
- на экране входа появилась кнопка `Войти через другое устройство`;
|
- на экране входа появилась кнопка `Войти через другое устройство`;
|
||||||
- на доверённом устройстве в `Подключить устройство` появилась кнопка `Подключить по коду`;
|
- на доверённом устройстве в `Подключить устройство` появилась кнопка `Подключить по коду`;
|
||||||
- доверённое устройство может включить pairing-пароль, увидеть заявки, подтвердить подключение только с `device key` или с передачей выбранных ключей.
|
- доверённое устройство может включить pairing с доп. паролем или без него, увидеть заявки, подтвердить подключение только с `device key` или с передачей выбранных ключей.
|
||||||
|
|
||||||
- что именно проверять:
|
- что именно проверять:
|
||||||
- на уже авторизованном устройстве включить pairing-пароль;
|
- на уже авторизованном устройстве включить pairing без доп. пароля;
|
||||||
- на новом устройстве открыть `Войти через другое устройство`, ввести `login + pairing password` и получить 7-значный код;
|
- на новом устройстве открыть `Войти через другое устройство`, оставить галочку доп. пароля выключенной и получить 7-значный код;
|
||||||
|
- отдельно включить pairing с доп. паролем;
|
||||||
|
- на новом устройстве открыть `Войти через другое устройство`, включить галочку доп. пароля, ввести `login + pairing password` и получить 7-значный код;
|
||||||
- на доверённом устройстве открыть `Подключить по коду`, найти заявку по коду и подтвердить её:
|
- на доверённом устройстве открыть `Подключить по коду`, найти заявку по коду и подтвердить её:
|
||||||
- без доп. ключей;
|
- без доп. ключей;
|
||||||
- с передачей выбранных ключей;
|
- с передачей выбранных ключей;
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
Цель:
|
Цель:
|
||||||
|
|
||||||
- новое устройство знает `login + pairing password`;
|
- новое устройство знает `login`, а `pairing password` используется только если он включён на доверённом устройстве;
|
||||||
- сервер использует пароль только как фильтр от мусора;
|
- сервер использует пароль только как фильтр от мусора;
|
||||||
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
|
- реальное доверие даёт любая уже онлайн доверенная сессия пользователя;
|
||||||
- сервер не выдаёт приватные ключи сам от себя.
|
- сервер не выдаёт приватные ключи сам от себя.
|
||||||
@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
## 3. Что именно делает сервер
|
## 3. Что именно делает сервер
|
||||||
|
|
||||||
- хранит включённость pairing и opaque `passwordHash`;
|
- хранит включённость pairing и optional opaque `passwordHash`;
|
||||||
- хранит pending/approved/rejected pairing-заявки;
|
- хранит pending/approved/rejected pairing-заявки;
|
||||||
- рассчитывает короткий код `shortCode` из `7` цифр;
|
- рассчитывает короткий код `shortCode` из `7` цифр;
|
||||||
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
|
- рассчитывает длинный `fingerprintB58` из `SHA-256` заявки;
|
||||||
@ -101,6 +101,6 @@
|
|||||||
|
|
||||||
Эта схема даёт нужное разделение доверия:
|
Эта схема даёт нужное разделение доверия:
|
||||||
|
|
||||||
- пароль на сервере только отсеивает лишних;
|
- пароль на сервере, если он включён, только отсеивает лишних;
|
||||||
- онлайн доверенная сессия решает, добавлять ли новую сессию;
|
- онлайн доверенная сессия решает, добавлять ли новую сессию;
|
||||||
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
|
- сервер остаётся маршрутизатором и хранилищем состояния, а не владельцем секретов.
|
||||||
|
|||||||
@ -55,9 +55,6 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
|
|||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PAYLOAD_TYPE", "payloadType должен быть 1, 2 или 3");
|
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_PAYLOAD_TYPE", "payloadType должен быть 1, 2 или 3");
|
||||||
}
|
}
|
||||||
String passwordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash());
|
String passwordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash());
|
||||||
if (passwordHash == null) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_PASSWORD_HASH", "Пустой passwordHash");
|
|
||||||
}
|
|
||||||
|
|
||||||
SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login);
|
SolanaUserEntry user = SolanaUsersDAO.getInstance().getByLogin(login);
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
@ -84,9 +81,14 @@ public class Net_StartEspPairing_Handler implements JsonMessageHandler {
|
|||||||
if (recentAttempts >= EspPairingSupport.REQUEST_RATE_LIMIT) {
|
if (recentAttempts >= EspPairingSupport.REQUEST_RATE_LIMIT) {
|
||||||
return NetExceptionResponseFactory.error(req, EspPairingSupport.STATUS_PAIRING_RATE_LIMIT, "PAIRING_RATE_LIMITED", "Слишком много pairing-запросов за короткое время");
|
return NetExceptionResponseFactory.error(req, EspPairingSupport.STATUS_PAIRING_RATE_LIMIT, "PAIRING_RATE_LIMITED", "Слишком много pairing-запросов за короткое время");
|
||||||
}
|
}
|
||||||
if (!settings.getPasswordHash().equals(passwordHash)) {
|
String configuredPasswordHash = settings.getPasswordHash() == null ? "" : settings.getPasswordHash().trim();
|
||||||
|
boolean requiresPassword = !configuredPasswordHash.isBlank();
|
||||||
|
if (requiresPassword && !configuredPasswordHash.equals(passwordHash)) {
|
||||||
return NetExceptionResponseFactory.error(req, 422, "PAIRING_PASSWORD_INVALID", "Неверный pairing-пароль");
|
return NetExceptionResponseFactory.error(req, 422, "PAIRING_PASSWORD_INVALID", "Неверный pairing-пароль");
|
||||||
}
|
}
|
||||||
|
if (!requiresPassword && passwordHash != null && !passwordHash.isBlank()) {
|
||||||
|
passwordHash = "";
|
||||||
|
}
|
||||||
|
|
||||||
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getRequesterClientPlatform());
|
String clientPlatform = AuthSessionTypeSupport.normalizeClientPlatform(req.getRequesterClientPlatform());
|
||||||
int ttlSeconds = EspPairingSupport.normalizeTtlSeconds(settings.getTtlSeconds());
|
int ttlSeconds = EspPairingSupport.normalizeTtlSeconds(settings.getTtlSeconds());
|
||||||
|
|||||||
@ -29,9 +29,6 @@ public class Net_UpsertEspPairingSettings_Handler implements JsonMessageHandler
|
|||||||
boolean enabled = req.getEnabled() != null && req.getEnabled();
|
boolean enabled = req.getEnabled() != null && req.getEnabled();
|
||||||
String passwordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash());
|
String passwordHash = EspPairingSupport.normalizeOpaqueHash(req.getPasswordHash());
|
||||||
int ttlSeconds = EspPairingSupport.normalizeTtlSeconds(req.getTtlSeconds());
|
int ttlSeconds = EspPairingSupport.normalizeTtlSeconds(req.getTtlSeconds());
|
||||||
if (enabled && (passwordHash == null || passwordHash.isBlank())) {
|
|
||||||
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "EMPTY_PASSWORD_HASH", "Для включения pairing нужен passwordHash");
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
EspPairingSettingsEntry entry = new EspPairingSettingsEntry();
|
EspPairingSettingsEntry entry = new EspPairingSettingsEntry();
|
||||||
|
|||||||
@ -79,6 +79,21 @@ public class IT_07_EspPairing {
|
|||||||
assertEquals("approved", JsonParsers.payloadText(statusResp, "state"));
|
assertEquals("approved", JsonParsers.payloadText(statusResp, "state"));
|
||||||
assertEquals("AQIDBA==", JsonParsers.payloadText(statusResp, "encryptedPayload"));
|
assertEquals("AQIDBA==", JsonParsers.payloadText(statusResp, "encryptedPayload"));
|
||||||
|
|
||||||
|
String upsertNoPasswordResp = clientWs.call(
|
||||||
|
"UpsertEspPairingSettings",
|
||||||
|
JsonBuilders.upsertEspPairingSettings(true, "", 180),
|
||||||
|
t
|
||||||
|
);
|
||||||
|
assertEquals(200, JsonParsers.status(upsertNoPasswordResp), "UpsertEspPairingSettings without password must be 200");
|
||||||
|
|
||||||
|
SessionMaterial requesterNoPasswordMaterial = newSessionMaterial();
|
||||||
|
String startNoPasswordResp = requesterWs.call(
|
||||||
|
"StartEspPairing",
|
||||||
|
JsonBuilders.startEspPairing(LOGIN, "", requesterNoPasswordMaterial.sessionKey(), 1, "Android", 1),
|
||||||
|
t
|
||||||
|
);
|
||||||
|
assertEquals(200, JsonParsers.status(startNoPasswordResp), "StartEspPairing without password must be 200");
|
||||||
|
|
||||||
String forbiddenResp = requesterWs.call(
|
String forbiddenResp = requesterWs.call(
|
||||||
"ListEspPairingRequests#anonymous",
|
"ListEspPairingRequests#anonymous",
|
||||||
JsonBuilders.listEspPairingRequests(),
|
JsonBuilders.listEspPairingRequests(),
|
||||||
@ -86,7 +101,7 @@ public class IT_07_EspPairing {
|
|||||||
);
|
);
|
||||||
assertErrorFormat(forbiddenResp, "ListEspPairingRequests", "PAIRING_REQUIRES_AUTH_SESSION");
|
assertErrorFormat(forbiddenResp, "ListEspPairingRequests", "PAIRING_REQUIRES_AUTH_SESSION");
|
||||||
|
|
||||||
r.ok("ESP pairing: обычная доверенная сессия увидела запрос и подтвердила зашифрованный payload");
|
r.ok("ESP pairing: доверенная сессия принимает заявки как с доп. паролем, так и без него");
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
r.fail("IT_07_EspPairing упал: " + e.getMessage());
|
r.fail("IT_07_EspPairing упал: " + e.getMessage());
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.195
|
client.version=1.2.196
|
||||||
server.version=1.2.184
|
server.version=1.2.185
|
||||||
|
|||||||
@ -42,7 +42,7 @@ import * as topupView from './pages/topup-view.js';
|
|||||||
import * as devnetTopupView from './pages/devnet-topup-view.js';
|
import * as devnetTopupView from './pages/devnet-topup-view.js';
|
||||||
import * as loginView from './pages/login-view.js?v=202606142055';
|
import * as loginView from './pages/login-view.js?v=202606142055';
|
||||||
import * as loginCameraView from './pages/login-camera-view.js';
|
import * as loginCameraView from './pages/login-camera-view.js';
|
||||||
import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606142055';
|
import * as loginOtherDeviceView from './pages/login-other-device-view.js?v=202606150010';
|
||||||
import * as loginPasswordView from './pages/login-password-view.js';
|
import * as loginPasswordView from './pages/login-password-view.js';
|
||||||
import * as keyStorageView from './pages/key-storage-view.js';
|
import * as keyStorageView from './pages/key-storage-view.js';
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ import * as serverSettingsView from './pages/server-settings-view.js';
|
|||||||
import * as toolsSettingsView from './pages/tools-settings-view.js';
|
import * as toolsSettingsView from './pages/tools-settings-view.js';
|
||||||
import * as deviceView from './pages/device-view.js?v=202606131435';
|
import * as deviceView from './pages/device-view.js?v=202606131435';
|
||||||
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
|
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
|
||||||
import * as devicePairingView from './pages/device-pairing-view.js?v=202606142055';
|
import * as devicePairingView from './pages/device-pairing-view.js?v=202606150010';
|
||||||
import * as deviceQrView from './pages/device-qr-view.js';
|
import * as deviceQrView from './pages/device-qr-view.js';
|
||||||
import * as deviceCameraView from './pages/device-camera-view.js';
|
import * as deviceCameraView from './pages/device-camera-view.js';
|
||||||
import * as showKeysView from './pages/show-keys-view.js';
|
import * as showKeysView from './pages/show-keys-view.js';
|
||||||
|
|||||||
@ -90,8 +90,12 @@ export function render({ navigate }) {
|
|||||||
settingsCard.className = 'card stack';
|
settingsCard.className = 'card stack';
|
||||||
settingsCard.innerHTML = `
|
settingsCard.innerHTML = `
|
||||||
<p class="field-label">Пароль подключения</p>
|
<p class="field-label">Пароль подключения</p>
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" id="pairing-use-password" />
|
||||||
|
использовать доп. пароль
|
||||||
|
</label>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="meta-muted">Задайте пароль, который новое устройство введёт перед получением кода.</span>
|
<span class="meta-muted" id="pairing-password-help">Если включено, новое устройство должно будет ввести этот пароль перед получением кода.</span>
|
||||||
<input class="input" id="pairing-password" type="password" autocomplete="new-password" placeholder="Новый pairing-пароль" />
|
<input class="input" id="pairing-password" type="password" autocomplete="new-password" placeholder="Новый pairing-пароль" />
|
||||||
</label>
|
</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -127,6 +131,8 @@ export function render({ navigate }) {
|
|||||||
status.style.display = 'none';
|
status.style.display = 'none';
|
||||||
|
|
||||||
const passwordInput = settingsCard.querySelector('#pairing-password');
|
const passwordInput = settingsCard.querySelector('#pairing-password');
|
||||||
|
const usePasswordInput = settingsCard.querySelector('#pairing-use-password');
|
||||||
|
const passwordHelpEl = settingsCard.querySelector('#pairing-password-help');
|
||||||
const enableBtn = settingsCard.querySelector('#enable-pairing-btn');
|
const enableBtn = settingsCard.querySelector('#enable-pairing-btn');
|
||||||
const disableBtn = settingsCard.querySelector('#disable-pairing-btn');
|
const disableBtn = settingsCard.querySelector('#disable-pairing-btn');
|
||||||
const keySummaryEl = keySummaryCard.querySelector('#pairing-key-summary');
|
const keySummaryEl = keySummaryCard.querySelector('#pairing-key-summary');
|
||||||
@ -134,6 +140,17 @@ export function render({ navigate }) {
|
|||||||
const refreshBtn = requestsCard.querySelector('#refresh-pairing-requests');
|
const refreshBtn = requestsCard.querySelector('#refresh-pairing-requests');
|
||||||
const requestsListEl = requestsCard.querySelector('#pairing-requests-list');
|
const requestsListEl = requestsCard.querySelector('#pairing-requests-list');
|
||||||
|
|
||||||
|
const syncPasswordUi = () => {
|
||||||
|
const usePassword = !!usePasswordInput.checked;
|
||||||
|
passwordInput.parentElement.style.display = usePassword ? '' : 'none';
|
||||||
|
passwordHelpEl.textContent = usePassword
|
||||||
|
? 'Если включено, новое устройство должно будет ввести этот пароль перед получением кода.'
|
||||||
|
: 'Если выключено, новое устройство сможет входить без доп. пароля.';
|
||||||
|
if (!usePassword) {
|
||||||
|
passwordInput.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderRequests = () => {
|
const renderRequests = () => {
|
||||||
const filterCode = normalizeCode(codeFilterInput.value);
|
const filterCode = normalizeCode(codeFilterInput.value);
|
||||||
const filtered = filterCode
|
const filtered = filterCode
|
||||||
@ -187,6 +204,7 @@ export function render({ navigate }) {
|
|||||||
enableBtn.disabled = flag;
|
enableBtn.disabled = flag;
|
||||||
disableBtn.disabled = flag;
|
disableBtn.disabled = flag;
|
||||||
refreshBtn.disabled = flag;
|
refreshBtn.disabled = flag;
|
||||||
|
usePasswordInput.disabled = flag;
|
||||||
};
|
};
|
||||||
|
|
||||||
const approveRequest = async (request, mode) => {
|
const approveRequest = async (request, mode) => {
|
||||||
@ -205,26 +223,33 @@ export function render({ navigate }) {
|
|||||||
await reloadRequests({ silent: true });
|
await reloadRequests({ silent: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usePasswordInput.addEventListener('change', syncPasswordUi);
|
||||||
|
|
||||||
settingsCard.addEventListener('click', async (event) => {
|
settingsCard.addEventListener('click', async (event) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
if (!(target instanceof HTMLElement)) return;
|
if (!(target instanceof HTMLElement)) return;
|
||||||
|
|
||||||
if (target.id === 'enable-pairing-btn') {
|
if (target.id === 'enable-pairing-btn') {
|
||||||
|
const usePassword = !!usePasswordInput.checked;
|
||||||
const password = String(passwordInput.value || '');
|
const password = String(passwordInput.value || '');
|
||||||
if (!password) {
|
if (usePassword && !password) {
|
||||||
setStatus(status, 'Введите pairing-пароль.', 'error');
|
setStatus(status, 'Введите pairing-пароль.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setButtonsBusy(true);
|
setButtonsBusy(true);
|
||||||
try {
|
try {
|
||||||
const passwordHash = await deriveEspPairingPasswordHash(state.session.login, password);
|
const passwordHash = usePassword
|
||||||
|
? await deriveEspPairingPasswordHash(state.session.login, password)
|
||||||
|
: '';
|
||||||
const payload = await authService.upsertEspPairingSettings({
|
const payload = await authService.upsertEspPairingSettings({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
ttlSeconds: 180,
|
ttlSeconds: 180,
|
||||||
});
|
});
|
||||||
setAuthInfo(`Подключение по коду включено. TTL: ${payload?.ttlSeconds || 180} сек.`);
|
setAuthInfo(`Подключение по коду включено. TTL: ${payload?.ttlSeconds || 180} сек.`);
|
||||||
setStatus(status, 'Подключение по коду включено или обновлено.', 'info');
|
setStatus(status, usePassword
|
||||||
|
? 'Подключение по коду включено с доп. паролем.'
|
||||||
|
: 'Подключение по коду включено без доп. пароля.', 'info');
|
||||||
passwordInput.value = '';
|
passwordInput.value = '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = toUserMessage(error, 'Не удалось включить pairing.');
|
const message = toUserMessage(error, 'Не удалось включить pairing.');
|
||||||
@ -297,6 +322,7 @@ export function render({ navigate }) {
|
|||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
try {
|
try {
|
||||||
|
syncPasswordUi();
|
||||||
await loadSavedKeys();
|
await loadSavedKeys();
|
||||||
await reloadRequests({ silent: true });
|
await reloadRequests({ silent: true });
|
||||||
cleanupEvent = authService.onEvent('IncomingEspPairingRequest', () => {
|
cleanupEvent = authService.onEvent('IncomingEspPairingRequest', () => {
|
||||||
|
|||||||
@ -68,12 +68,16 @@ export function render({ navigate }) {
|
|||||||
<span class="field-label">Логин</span>
|
<span class="field-label">Логин</span>
|
||||||
<input class="input" id="pair-login" type="text" autocomplete="username" placeholder="@login" value="${String(state.loginDraft.login || '')}" />
|
<input class="input" id="pair-login" type="text" autocomplete="username" placeholder="@login" value="${String(state.loginDraft.login || '')}" />
|
||||||
</label>
|
</label>
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" id="pair-use-password" />
|
||||||
|
использовать доп. пароль
|
||||||
|
</label>
|
||||||
<label class="stack">
|
<label class="stack">
|
||||||
<span class="field-label">Пароль подключения</span>
|
<span class="field-label">Пароль подключения</span>
|
||||||
<input class="input" id="pair-password" type="password" autocomplete="current-password" placeholder="Пароль, заданный на другом устройстве" />
|
<input class="input" id="pair-password" type="password" autocomplete="current-password" placeholder="Пароль, заданный на другом устройстве" />
|
||||||
</label>
|
</label>
|
||||||
<button class="primary-btn" type="button" id="pair-start-btn">Получить код</button>
|
<button class="primary-btn" type="button" id="pair-start-btn">Получить код</button>
|
||||||
<p class="meta-muted">Сначала вводится ваш логин и pairing-пароль. После этого появится 7-значный код для подтверждения на уже подключённом устройстве.</p>
|
<p class="meta-muted" id="pair-mode-hint">Сначала вводится ваш логин. Если на доверённом устройстве включён доп. пароль, включите галочку и введите его. После этого появится 7-значный код для подтверждения.</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const status = document.createElement('p');
|
const status = document.createElement('p');
|
||||||
@ -86,13 +90,26 @@ export function render({ navigate }) {
|
|||||||
resultWrap.innerHTML = codeCardHtml();
|
resultWrap.innerHTML = codeCardHtml();
|
||||||
|
|
||||||
const loginInput = formCard.querySelector('#pair-login');
|
const loginInput = formCard.querySelector('#pair-login');
|
||||||
|
const usePasswordInput = formCard.querySelector('#pair-use-password');
|
||||||
const passwordInput = formCard.querySelector('#pair-password');
|
const passwordInput = formCard.querySelector('#pair-password');
|
||||||
const startBtn = formCard.querySelector('#pair-start-btn');
|
const startBtn = formCard.querySelector('#pair-start-btn');
|
||||||
|
const modeHintEl = formCard.querySelector('#pair-mode-hint');
|
||||||
const shortCodeEl = resultWrap.querySelector('#pairing-short-code');
|
const shortCodeEl = resultWrap.querySelector('#pairing-short-code');
|
||||||
const statusHintEl = resultWrap.querySelector('#pairing-status-hint');
|
const statusHintEl = resultWrap.querySelector('#pairing-status-hint');
|
||||||
const onlineHintEl = resultWrap.querySelector('#pairing-online-hint');
|
const onlineHintEl = resultWrap.querySelector('#pairing-online-hint');
|
||||||
const expireHintEl = resultWrap.querySelector('#pairing-expire-hint');
|
const expireHintEl = resultWrap.querySelector('#pairing-expire-hint');
|
||||||
|
|
||||||
|
const syncPasswordUi = () => {
|
||||||
|
const usePassword = !!usePasswordInput.checked;
|
||||||
|
passwordInput.parentElement.style.display = usePassword ? '' : 'none';
|
||||||
|
modeHintEl.textContent = usePassword
|
||||||
|
? 'Введите логин и доп. пароль, который был задан на доверённом устройстве.'
|
||||||
|
: 'Введите логин. Если на доверённом устройстве пароль не задан, вход пойдёт без доп. пароля.';
|
||||||
|
if (!usePassword) {
|
||||||
|
passwordInput.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
if (pollTimer) {
|
if (pollTimer) {
|
||||||
window.clearTimeout(pollTimer);
|
window.clearTimeout(pollTimer);
|
||||||
@ -167,14 +184,18 @@ export function render({ navigate }) {
|
|||||||
}, 2200);
|
}, 2200);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usePasswordInput.addEventListener('change', syncPasswordUi);
|
||||||
|
syncPasswordUi();
|
||||||
|
|
||||||
startBtn.addEventListener('click', async () => {
|
startBtn.addEventListener('click', async () => {
|
||||||
const login = String(loginInput.value || '').trim();
|
const login = String(loginInput.value || '').trim();
|
||||||
|
const usePassword = !!usePasswordInput.checked;
|
||||||
const password = String(passwordInput.value || '');
|
const password = String(passwordInput.value || '');
|
||||||
if (!login) {
|
if (!login) {
|
||||||
setStatus(status, 'Введите логин.', 'error');
|
setStatus(status, 'Введите логин.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!password) {
|
if (usePassword && !password) {
|
||||||
setStatus(status, 'Введите пароль подключения.', 'error');
|
setStatus(status, 'Введите пароль подключения.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -195,7 +216,9 @@ export function render({ navigate }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requesterMaterial = await createRequesterPairingMaterial();
|
requesterMaterial = await createRequesterPairingMaterial();
|
||||||
const passwordHash = await deriveEspPairingPasswordHash(login, password);
|
const passwordHash = usePassword
|
||||||
|
? await deriveEspPairingPasswordHash(login, password)
|
||||||
|
: '';
|
||||||
const payload = await authService.startEspPairing({
|
const payload = await authService.startEspPairing({
|
||||||
login,
|
login,
|
||||||
passwordHash,
|
passwordHash,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user