diff --git a/src/antigravity_provider/router/profile_oauth.py b/src/antigravity_provider/router/profile_oauth.py index 6b6ad91..59dfa0e 100644 --- a/src/antigravity_provider/router/profile_oauth.py +++ b/src/antigravity_provider/router/profile_oauth.py @@ -299,6 +299,38 @@ class ProfileOAuthSession: return False, "Пожалуйста, вставьте полный URL из адресной строки браузера." 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?...) if not raw_url.startswith("http://") and not raw_url.startswith("https://"): raw_url = "http://" + raw_url diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index e0d02c6..f93166e 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -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 ── // // Сервер, привязанный не к localhost, требует X-Hub-Token. Раньше клиент этого @@ -2143,12 +2184,12 @@ async function startDeviceAuth(providerId) {
- +
2. Введите код:
${escapeHtml(d.code || '')}
- +
3. Подтвердите доступ — окно обновится само. Код живёт недолго, не откладывайте. @@ -2258,11 +2299,37 @@ async function startRedirectAuth(providerId) { : 'Вставьте код, показанный на странице:'; 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 + ? `` + : ''; + const localNote = pastesUrl && d.redirect_uri ? `
- После подтверждения браузер уйдёт на ${escapeHtml(d.redirect_uri)}. - Если Hub работает на другой машине, страница не откроется — это ожидаемо. - Нужен сам адрес из строки браузера, а не содержимое страницы. + Без проброса браузер после подтверждения уйдёт на ${escapeHtml(d.redirect_uri)} + и покажет «страница недоступна» — это ожидаемо, хаб на другой машине. + Скопируйте из адресной строки весь адрес целиком либо только значение + code= — принимается и то, и другое.
` : ''; @@ -2271,13 +2338,14 @@ async function startRedirectAuth(providerId) {
- +
2. ${escapeHtml(label)}
+ ${tunnelNote} ${localNote}
Слот: ${escapeHtml(d.profile_id || '—')}. Ссылка действует 20 минут.