SHA256
Доработать UX и отмену pairing по коду
This commit is contained in:
@@ -38,10 +38,11 @@ function codeCardHtml() {
|
||||
`;
|
||||
}
|
||||
|
||||
function formatExpiresAt(ms) {
|
||||
const ts = Number(ms || 0);
|
||||
if (!Number.isFinite(ts) || ts <= 0) return '';
|
||||
return new Date(ts).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
function formatRemaining(ms) {
|
||||
const safe = Math.max(0, Math.floor(Number(ms || 0) / 1000));
|
||||
const minutes = Math.floor(safe / 60);
|
||||
const seconds = safe % 60;
|
||||
return `${minutes} мин ${seconds} сек`;
|
||||
}
|
||||
|
||||
function resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl) {
|
||||
@@ -56,8 +57,10 @@ export function render({ navigate }) {
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack';
|
||||
let pollTimer = 0;
|
||||
let countdownTimer = 0;
|
||||
let activePairingId = '';
|
||||
let requesterMaterial = null;
|
||||
let activeExpiresAtMs = 0;
|
||||
let isDisposed = false;
|
||||
|
||||
clearAuthMessages();
|
||||
@@ -73,8 +76,8 @@ export function render({ navigate }) {
|
||||
formCard.className = 'card stack';
|
||||
formCard.innerHTML = `
|
||||
<label class="stack">
|
||||
<span class="field-label">Логин</span>
|
||||
<input class="input" id="pair-login" type="text" autocomplete="username" placeholder="@login" value="${String(state.loginDraft.login || '')}" />
|
||||
<span class="field-label">Введите логин</span>
|
||||
<input class="input" id="pair-login" type="text" autocomplete="username" placeholder="" value="" />
|
||||
</label>
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" id="pair-use-password" />
|
||||
@@ -85,7 +88,7 @@ export function render({ navigate }) {
|
||||
<input class="input" id="pair-password" type="password" autocomplete="current-password" placeholder="Пароль, заданный на другом устройстве" />
|
||||
</label>
|
||||
<button class="primary-btn" type="button" id="pair-start-btn">Получить код</button>
|
||||
<p class="meta-muted" id="pair-mode-hint">Сначала вводится ваш логин. Если на доверённом устройстве включён доп. пароль, включите галочку и введите его. После этого появится 7-значный код для подтверждения.</p>
|
||||
<p class="meta-muted" id="pair-mode-hint">Получить код для входа можно только если сейчас есть хотя бы одно другое активное устройство этого пользователя в сети.</p>
|
||||
`;
|
||||
|
||||
const status = document.createElement('p');
|
||||
@@ -106,13 +109,18 @@ export function render({ navigate }) {
|
||||
const statusHintEl = resultWrap.querySelector('#pairing-status-hint');
|
||||
const onlineHintEl = resultWrap.querySelector('#pairing-online-hint');
|
||||
const expireHintEl = resultWrap.querySelector('#pairing-expire-hint');
|
||||
const cancelBtn = document.createElement('button');
|
||||
cancelBtn.className = 'ghost-btn';
|
||||
cancelBtn.type = 'button';
|
||||
cancelBtn.textContent = 'Отмена';
|
||||
cancelBtn.style.display = 'none';
|
||||
|
||||
const syncPasswordUi = () => {
|
||||
const usePassword = !!usePasswordInput.checked;
|
||||
passwordInput.parentElement.style.display = usePassword ? '' : 'none';
|
||||
modeHintEl.textContent = usePassword
|
||||
? 'Введите логин и доп. пароль, который был задан на доверённом устройстве.'
|
||||
: 'Введите логин. Если на доверённом устройстве пароль не задан, вход пойдёт без доп. пароля.';
|
||||
? 'Получить код для входа можно только если сейчас есть хотя бы одно другое активное устройство этого пользователя в сети. Если на доверённом устройстве включён доп. пароль, введите его.'
|
||||
: 'Получить код для входа можно только если сейчас есть хотя бы одно другое активное устройство этого пользователя в сети.';
|
||||
if (!usePassword) {
|
||||
passwordInput.value = '';
|
||||
}
|
||||
@@ -125,6 +133,45 @@ export function render({ navigate }) {
|
||||
}
|
||||
};
|
||||
|
||||
const stopCountdown = () => {
|
||||
if (countdownTimer) {
|
||||
window.clearInterval(countdownTimer);
|
||||
countdownTimer = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const updateCountdown = () => {
|
||||
const leftMs = activeExpiresAtMs - Date.now();
|
||||
if (leftMs <= 0) {
|
||||
stopPolling();
|
||||
stopCountdown();
|
||||
activePairingId = '';
|
||||
activeExpiresAtMs = 0;
|
||||
startBtn.disabled = false;
|
||||
cancelBtn.style.display = 'none';
|
||||
resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl);
|
||||
setStatus(status, 'Время ожидания истекло. Получите новый код.', 'error');
|
||||
return;
|
||||
}
|
||||
expireHintEl.textContent = `Код действителен ещё ${formatRemaining(leftMs)}.`;
|
||||
};
|
||||
|
||||
const startCountdown = (expiresAtMs) => {
|
||||
activeExpiresAtMs = Number(expiresAtMs || 0);
|
||||
stopCountdown();
|
||||
updateCountdown();
|
||||
countdownTimer = window.setInterval(updateCountdown, 1000);
|
||||
};
|
||||
|
||||
const clearActivePairing = () => {
|
||||
stopPolling();
|
||||
stopCountdown();
|
||||
activePairingId = '';
|
||||
activeExpiresAtMs = 0;
|
||||
cancelBtn.style.display = 'none';
|
||||
resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl);
|
||||
};
|
||||
|
||||
const finalizeAuthorizedLogin = async (keys, login) => {
|
||||
const session = await authService.createSessionFromImportedSecrets(login, keys);
|
||||
await clearStoredMessages().catch(() => {});
|
||||
@@ -159,6 +206,7 @@ export function render({ navigate }) {
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'approved') {
|
||||
stopCountdown();
|
||||
setStatus(status, 'Заявка подтверждена. Подключаем устройство...', 'info');
|
||||
const decoded = await decryptPairingPayloadFromEnvelope(payload?.encryptedPayload, requesterMaterial);
|
||||
if (String(decoded?.type || '') !== 'shine-esp-pairing-transfer') {
|
||||
@@ -168,18 +216,20 @@ export function render({ navigate }) {
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'rejected') {
|
||||
stopPolling();
|
||||
activePairingId = '';
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl);
|
||||
setStatus(status, 'Подключение отклонено на доверенном устройстве.', 'error');
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'canceled') {
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
setStatus(status, 'Ожидание подключения отменено.', 'error');
|
||||
return;
|
||||
}
|
||||
if (stateValue === 'expired') {
|
||||
stopPolling();
|
||||
activePairingId = '';
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl);
|
||||
setStatus(status, 'Время ожидания истекло. Получите новый код.', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -215,9 +265,7 @@ export function render({ navigate }) {
|
||||
setAuthError('');
|
||||
setAuthInfo('');
|
||||
setStatus(status, 'Проверяем пользователя и создаём pairing-заявку...', 'info');
|
||||
stopPolling();
|
||||
activePairingId = '';
|
||||
resetCodeCard(resultWrap, shortCodeEl, statusHintEl, onlineHintEl, expireHintEl);
|
||||
clearActivePairing();
|
||||
|
||||
try {
|
||||
await authService.reconnect(state.entrySettings.shineServer);
|
||||
@@ -247,10 +295,9 @@ export function render({ navigate }) {
|
||||
onlineHintEl.textContent = payload?.trustedSessionOnline
|
||||
? 'Сейчас есть хотя бы одна онлайн доверенная сессия, которая может принять заявку.'
|
||||
: 'Сейчас нет онлайн доверенной сессии. Заявка будет ждать, пока пользователь откроет уже подключённое устройство.';
|
||||
expireHintEl.textContent = payload?.expiresAtMs
|
||||
? `Код действует до ${formatExpiresAt(payload.expiresAtMs)}.`
|
||||
: '';
|
||||
resultWrap.style.display = '';
|
||||
cancelBtn.style.display = '';
|
||||
startCountdown(payload?.expiresAtMs);
|
||||
state.loginDraft.login = login;
|
||||
setStatus(status, 'Код создан. Ожидаем подтверждение на другом устройстве...', 'info');
|
||||
schedulePoll();
|
||||
@@ -264,11 +311,40 @@ export function render({ navigate }) {
|
||||
}
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', async () => {
|
||||
if (!activePairingId || !requesterMaterial?.sessionKey) {
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
cancelBtn.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
cancelBtn.disabled = true;
|
||||
try {
|
||||
await authService.cancelEspPairing(activePairingId, requesterMaterial.sessionKey);
|
||||
clearActivePairing();
|
||||
startBtn.disabled = false;
|
||||
setStatus(status, 'Ожидание подключения отменено.', 'info');
|
||||
} catch (error) {
|
||||
const message = toUserMessage(error, 'Не удалось отменить ожидание подключения.');
|
||||
setAuthError(message);
|
||||
setStatus(status, message, 'error');
|
||||
} finally {
|
||||
cancelBtn.disabled = false;
|
||||
cancelBtn.style.display = activePairingId ? '' : 'none';
|
||||
}
|
||||
});
|
||||
|
||||
screen.cleanup = () => {
|
||||
isDisposed = true;
|
||||
stopPolling();
|
||||
stopCountdown();
|
||||
};
|
||||
|
||||
const resultActions = document.createElement('div');
|
||||
resultActions.className = 'row';
|
||||
resultActions.append(cancelBtn);
|
||||
resultWrap.append(resultActions);
|
||||
|
||||
screen.append(formCard, status, resultWrap);
|
||||
return screen;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user