fix(web): кнопка копирования молча не работала по сети, а возврат вёл в тупик
Владелец открыл хаб по сети и не смог войти в Antigravity: кнопка «Копировать» не давала ничего, а после подтверждения доступа браузер уходил на 127.0.0.1:51121 его собственной машины и показывал «страница недоступна». Два дефекта. 1. navigator.clipboard существует только в защищённом контексте — HTTPS или localhost. По http://192.168.1.81:5800 его нет вовсе, и три кнопки копирования не работали. Хуже: промис никто не проверял, поэтому они показывали «Ссылка скопирована», ничего не скопировав. Подтверждено измерением на живой странице по сетевому адресу: isSecureContext=false, navigator.clipboard отсутствует. Добавлен общий помощник с запасным execCommand('copy') и честным сообщением при неудаче. 2. Адрес возврата вёл в тупик, из которого код ещё надо было выковырять со страницы ошибки, где он часто обрезан. Теперь, когда хаб открыт не с этой машины, показывается готовая команда проброса порта возврата: тогда слушатель хаба принимает возврат сам и вставлять ничего не нужно. Порт и адрес берутся из ответа сервера и window.location, не зашиты. Запасным путём принимается и голый код: не только полный адрес. Признак адреса — "?" или "://", но НЕ слэш: коды Google сами содержат его и начинаются с "4/0A...". Первая версия условия их отсекала — поймано тестом. Добавлена проверка правдоподобия, иначе произвольный текст уходил на обмен и давал невнятную ошибку провайдера вместо подсказки. Проверено исполнением: голый код, полный адрес и вставка без протокола принимаются; русский текст, короткая строка и строка с пробелом отвергаются без обращения к провайдеру. Подсказка о пробросе отрисована на живой странице, открытой по сетевому адресу. 432 passed, ruff чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
de7c7132ae
commit
0800ca94e0
2 changed files with 106 additions and 6 deletions
|
|
@ -299,6 +299,38 @@ class ProfileOAuthSession:
|
||||||
return False, "Пожалуйста, вставьте полный URL из адресной строки браузера."
|
return False, "Пожалуйста, вставьте полный URL из адресной строки браузера."
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Голый код без адреса. Браузер после подтверждения уходит на
|
||||||
|
# 127.0.0.1:<порт>, и если хаб на другой машине, страница не
|
||||||
|
# открывается: адрес приходится доставать со страницы ошибки, где
|
||||||
|
# он часто обрезан. Значение code= владелец видит и может взять
|
||||||
|
# только его. Собственный state подставляем сознательно — вставка
|
||||||
|
# ручная, в рамках уже открытой сессии этого же владельца.
|
||||||
|
# Признак адреса — "?" или "://", но НЕ слэш: коды Google сами
|
||||||
|
# содержат его и начинаются с "4/0A...". Вставка без протокола
|
||||||
|
# (127.0.0.1:49725/oauth-callback?code=...) всё равно несёт "?".
|
||||||
|
if "://" not in raw_url and "?" not in raw_url:
|
||||||
|
# Не всё, что не адрес, — код. Без проверки правдоподобия
|
||||||
|
# произвольный текст уходил бы на обмен, давая невнятную
|
||||||
|
# ошибку от провайдера вместо понятной подсказки.
|
||||||
|
looks_like_code = (
|
||||||
|
len(raw_url) >= 10
|
||||||
|
and not any(ch.isspace() for ch in raw_url)
|
||||||
|
and all(33 <= ord(ch) <= 126 for ch in raw_url)
|
||||||
|
)
|
||||||
|
if not looks_like_code:
|
||||||
|
return False, (
|
||||||
|
"Это не похоже ни на адрес возврата, ни на код. "
|
||||||
|
"Вставьте адрес из строки браузера целиком либо только "
|
||||||
|
"значение code= из него."
|
||||||
|
)
|
||||||
|
logger.info("OAuth manual input looks like a bare code")
|
||||||
|
return self.handle_callback(
|
||||||
|
code=raw_url,
|
||||||
|
state=self.state,
|
||||||
|
error=None,
|
||||||
|
source="manual",
|
||||||
|
)
|
||||||
|
|
||||||
# Handle potential protocol-less paste (e.g. 127.0.0.1:49725/oauth-callback?...)
|
# Handle potential protocol-less paste (e.g. 127.0.0.1:49725/oauth-callback?...)
|
||||||
if not raw_url.startswith("http://") and not raw_url.startswith("https://"):
|
if not raw_url.startswith("http://") and not raw_url.startswith("https://"):
|
||||||
raw_url = "http://" + raw_url
|
raw_url = "http://" + raw_url
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,47 @@ function startPolling() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── КОПИРОВАНИЕ В БУФЕР ──
|
||||||
|
//
|
||||||
|
// navigator.clipboard существует только в защищённом контексте: HTTPS или
|
||||||
|
// localhost. Когда хаб открыт по сети (http://192.168.1.81:5800), его нет
|
||||||
|
// вовсе, и кнопки «Копировать» молча не работали — хуже того, показывали
|
||||||
|
// «Ссылка скопирована», потому что промис никто не проверял.
|
||||||
|
// Запасной путь — execCommand('copy'), он работает и по HTTP.
|
||||||
|
async function copyToClipboard(text, okMessage) {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
showToast(okMessage || 'Скопировано', 'success');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('clipboard API недоступен:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text;
|
||||||
|
ta.setAttribute('readonly', '');
|
||||||
|
ta.style.position = 'fixed';
|
||||||
|
ta.style.top = '-1000px';
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
const ok = document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
if (ok) {
|
||||||
|
showToast(okMessage || 'Скопировано', 'success');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('execCommand copy не сработал:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Молчать нельзя: владелец решит, что скопировалось, и вставит старое.
|
||||||
|
showToast('Скопировать не удалось — выделите текст в поле и нажмите Ctrl+C', 'warning');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// ── ЗАПРОС ТОКЕНА ПРИ 401 ──
|
// ── ЗАПРОС ТОКЕНА ПРИ 401 ──
|
||||||
//
|
//
|
||||||
// Сервер, привязанный не к localhost, требует X-Hub-Token. Раньше клиент этого
|
// Сервер, привязанный не к localhost, требует X-Hub-Token. Раньше клиент этого
|
||||||
|
|
@ -2143,12 +2184,12 @@ async function startDeviceAuth(providerId) {
|
||||||
<div style="display:flex; gap:8px; margin-bottom:12px;">
|
<div style="display:flex; gap:8px; margin-bottom:12px;">
|
||||||
<input type="text" class="input-text" style="flex:1;" id="wiz-auth-url" value="${escapeHtml(d.url || '')}" readonly>
|
<input type="text" class="input-text" style="flex:1;" id="wiz-auth-url" value="${escapeHtml(d.url || '')}" readonly>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window.open(document.getElementById('wiz-auth-url').value, '_blank')">Открыть</button>
|
<button class="btn btn-secondary btn-sm" onclick="window.open(document.getElementById('wiz-auth-url').value, '_blank')">Открыть</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="navigator.clipboard.writeText(document.getElementById('wiz-auth-url').value); showToast('Ссылка скопирована', 'success');">Копировать</button>
|
<button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('wiz-auth-url').value, 'Ссылка скопирована')">Копировать</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight:700; margin-bottom:6px;">2. Введите код:</div>
|
<div style="font-weight:700; margin-bottom:6px;">2. Введите код:</div>
|
||||||
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
|
<div style="display:flex; align-items:center; gap:12px; margin-bottom:12px;">
|
||||||
<div style="font-family:var(--font-mono); font-size:22px; font-weight:700; color:var(--text-accent); letter-spacing:2px;" id="wiz-auth-code">${escapeHtml(d.code || '')}</div>
|
<div style="font-family:var(--font-mono); font-size:22px; font-weight:700; color:var(--text-accent); letter-spacing:2px;" id="wiz-auth-code">${escapeHtml(d.code || '')}</div>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="navigator.clipboard.writeText(document.getElementById('wiz-auth-code').innerText); showToast('Код скопирован', 'success');">Копировать код</button>
|
<button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('wiz-auth-code').innerText, 'Код скопирован')">Копировать код</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="device-auth-status" style="font-size:12px; color:var(--text-muted);">
|
<div id="device-auth-status" style="font-size:12px; color:var(--text-muted);">
|
||||||
3. Подтвердите доступ — окно обновится само. Код живёт недолго, не откладывайте.
|
3. Подтвердите доступ — окно обновится само. Код живёт недолго, не откладывайте.
|
||||||
|
|
@ -2258,11 +2299,37 @@ async function startRedirectAuth(providerId) {
|
||||||
: 'Вставьте код, показанный на странице:';
|
: 'Вставьте код, показанный на странице:';
|
||||||
const placeholder = pastesUrl ? 'http://127.0.0.1:…/oauth-callback?code=…' : 'Код со страницы провайдера';
|
const placeholder = pastesUrl ? 'http://127.0.0.1:…/oauth-callback?code=…' : 'Код со страницы провайдера';
|
||||||
|
|
||||||
|
// Если хаб открыт не с этой же машины, браузер после подтверждения уйдёт на
|
||||||
|
// 127.0.0.1:<порт> СВОЕЙ машины, где никто не слушает: получается тупик,
|
||||||
|
// из которого адрес с кодом ещё надо как-то выковырять. Проброс этого порта
|
||||||
|
// убирает проблему целиком — возврат попадает прямо в слушатель хаба и вход
|
||||||
|
// завершается сам. Показываем готовую команду, а вставку оставляем запасным
|
||||||
|
// путём.
|
||||||
|
const host = window.location.hostname;
|
||||||
|
const isRemote = host !== '127.0.0.1' && host !== 'localhost' && host !== '';
|
||||||
|
const cbPort = d.port || 0;
|
||||||
|
|
||||||
|
const tunnelNote = pastesUrl && isRemote && cbPort
|
||||||
|
? `<div class="modal-feedback info" style="margin-top:10px; font-size:12px;">
|
||||||
|
<strong>Проще всего — пробросить порт возврата.</strong> Выполните у себя
|
||||||
|
в терминале, до подтверждения доступа:
|
||||||
|
<div style="display:flex; gap:6px; margin:6px 0;">
|
||||||
|
<input type="text" class="input-text" style="flex:1; font-family:var(--font-mono); font-size:11px;"
|
||||||
|
id="wiz-redirect-tunnel" readonly
|
||||||
|
value="ssh -L ${cbPort}:127.0.0.1:${cbPort} ${escapeHtml(host)}">
|
||||||
|
<button class="btn btn-secondary btn-sm"
|
||||||
|
onclick="copyToClipboard(document.getElementById('wiz-redirect-tunnel').value, 'Команда скопирована')">Копировать</button>
|
||||||
|
</div>
|
||||||
|
Тогда вход завершится сам и вставлять ничего не придётся.
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
const localNote = pastesUrl && d.redirect_uri
|
const localNote = pastesUrl && d.redirect_uri
|
||||||
? `<div style="font-size:12px; color:var(--text-muted); margin-top:8px;">
|
? `<div style="font-size:12px; color:var(--text-muted); margin-top:8px;">
|
||||||
После подтверждения браузер уйдёт на <code>${escapeHtml(d.redirect_uri)}</code>.
|
Без проброса браузер после подтверждения уйдёт на <code>${escapeHtml(d.redirect_uri)}</code>
|
||||||
Если Hub работает на другой машине, страница не откроется — это ожидаемо.
|
и покажет «страница недоступна» — это ожидаемо, хаб на другой машине.
|
||||||
Нужен сам адрес из строки браузера, а не содержимое страницы.
|
Скопируйте из адресной строки весь адрес целиком либо только значение
|
||||||
|
<code>code=</code> — принимается и то, и другое.
|
||||||
</div>`
|
</div>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
|
|
@ -2271,13 +2338,14 @@ async function startRedirectAuth(providerId) {
|
||||||
<div style="display:flex; gap:8px; margin-bottom:12px;">
|
<div style="display:flex; gap:8px; margin-bottom:12px;">
|
||||||
<input type="text" class="input-text" style="flex:1;" id="wiz-redirect-url" value="${escapeHtml(d.url || '')}" readonly>
|
<input type="text" class="input-text" style="flex:1;" id="wiz-redirect-url" value="${escapeHtml(d.url || '')}" readonly>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="window.open(document.getElementById('wiz-redirect-url').value, '_blank')">Открыть</button>
|
<button class="btn btn-secondary btn-sm" onclick="window.open(document.getElementById('wiz-redirect-url').value, '_blank')">Открыть</button>
|
||||||
<button class="btn btn-secondary btn-sm" onclick="navigator.clipboard.writeText(document.getElementById('wiz-redirect-url').value); showToast('Ссылка скопирована', 'success');">Копировать</button>
|
<button class="btn btn-secondary btn-sm" onclick="copyToClipboard(document.getElementById('wiz-redirect-url').value, 'Ссылка скопирована')">Копировать</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-weight:700; margin-bottom:6px;">2. ${escapeHtml(label)}</div>
|
<div style="font-weight:700; margin-bottom:6px;">2. ${escapeHtml(label)}</div>
|
||||||
<div style="display:flex; gap:8px; margin-bottom:6px;">
|
<div style="display:flex; gap:8px; margin-bottom:6px;">
|
||||||
<input type="text" class="input-text" style="flex:1;" id="wiz-redirect-paste" placeholder="${escapeHtml(placeholder)}">
|
<input type="text" class="input-text" style="flex:1;" id="wiz-redirect-paste" placeholder="${escapeHtml(placeholder)}">
|
||||||
<button class="btn btn-primary btn-sm" onclick="submitRedirectCallback()">Завершить вход</button>
|
<button class="btn btn-primary btn-sm" onclick="submitRedirectCallback()">Завершить вход</button>
|
||||||
</div>
|
</div>
|
||||||
|
${tunnelNote}
|
||||||
${localNote}
|
${localNote}
|
||||||
<div id="redirect-auth-status" style="font-size:12px; color:var(--text-muted); margin-top:10px;">
|
<div id="redirect-auth-status" style="font-size:12px; color:var(--text-muted); margin-top:10px;">
|
||||||
Слот: ${escapeHtml(d.profile_id || '—')}. Ссылка действует 20 минут.
|
Слот: ${escapeHtml(d.profile_id || '—')}. Ссылка действует 20 минут.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue