diff --git a/docs-site/src/content/docs/fr/guides/web-dashboard.md b/docs-site/src/content/docs/fr/guides/web-dashboard.md index 159a972b2d0..c3c3fc7f562 100644 --- a/docs-site/src/content/docs/fr/guides/web-dashboard.md +++ b/docs-site/src/content/docs/fr/guides/web-dashboard.md @@ -33,9 +33,12 @@ automatiquement `~/.opencodex/admin-api-token`). Lorsqu'un tableau de bord distant exige cet identifiant, il présente un formulaire de mot de passe standard, ce qui permet au gestionnaire de mots de passe du navigateur de proposer son enregistrement et son -remplissage automatique. Le tableau de bord lui-même ne conserve le jeton qu'en mémoire et ne l'écrit ni -dans `localStorage` ni dans `sessionStorage` ; son enregistrement dépend entièrement du navigateur ou du -gestionnaire de mots de passe. +remplissage automatique. Par défaut, le tableau de bord ne conserve le jeton qu'en mémoire. Choisir +**Mémoriser sur cet appareil** autorise le stockage du jeton complet en clair dans `localStorage`. Tout +script de même origine et toute personne ayant accès à l'appareil peuvent le lire : ne l'activez pas sur un +appareil partagé. **Oublier le jeton administrateur enregistré**, à côté de Déconnexion, supprime cette valeur. +La valeur enregistrée est stockée par serveur, selon son origin et son transport, et n'est +renvoyée qu'au serveur pour lequel elle a été enregistrée. ## Barre de résumé des quotas diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index 3645ae7c1d3..f9a7112fb66 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -31,9 +31,12 @@ the admin token (`OPENCODEX_ADMIN_AUTH_TOKEN`, or the auto-generated `~/.opencodex/admin-api-token` file). When a remote dashboard needs that credential, it presents a standard password form so a browser -password manager can offer to save and autofill it. The dashboard itself still keeps the token only -in memory and does not write it to `localStorage` or `sessionStorage`; whether it is saved is entirely -the browser or password manager's decision. +password manager can offer to save and autofill it. By default, the dashboard keeps the token only +in memory. Selecting **Remember on this device** opts in to storing the full token as plaintext in +`localStorage`. Any same-origin script and anyone with access to the device can read it, so do not +enable it on a shared device. **Forget remembered admin token**, beside Logout, removes that value. +The remembered value is stored per server, scoped by server origin and transport, and is only +re-sent to the server it was saved for. ### Finding the admin token diff --git a/docs-site/src/content/docs/ja/guides/web-dashboard.md b/docs-site/src/content/docs/ja/guides/web-dashboard.md index b5b14513e46..eb187af9b63 100644 --- a/docs-site/src/content/docs/ja/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ja/guides/web-dashboard.md @@ -25,7 +25,8 @@ bun run dev:gui `localhost` や `127.0.0.1` などのループバックアドレスで開いたダッシュボードは、短時間有効な GUI セッションを自動的に受け取るため、通常はトークン入力が不要です。ループバック以外のホストで公開する場合は、`OPENCODEX_ADMIN_AUTH_TOKEN`、または自動生成される `~/.opencodex/admin-api-token` ファイルの管理トークンが必要です。 -リモートダッシュボードでは標準のパスワードフォームが表示され、ブラウザのパスワードマネージャーで保存・自動入力できます。ダッシュボード自体はトークンをメモリ内だけに保持し、`localStorage` や `sessionStorage` には書き込みません。保存するかどうかはブラウザまたはパスワードマネージャーだけが決定します。 +リモートダッシュボードでは標準のパスワードフォームが表示され、ブラウザのパスワードマネージャーで保存・自動入力できます。既定では、ダッシュボードはトークンをメモリ内だけに保持します。**この端末で記憶する** を選ぶと、完全なトークンを平文で `localStorage` に保存することに同意します。同一オリジンのスクリプトと端末にアクセスできる人は誰でも読み取れるため、共有端末では有効にしないでください。ログアウトの横にある **保存した管理者トークンを削除** でこの値を削除できます。 +記憶された値はサーバーごとに、サーバーの origin とトランスポートで区切って保存され、保存先のサーバーにのみ再送信されます。 ## クォータ概要バー diff --git a/docs-site/src/content/docs/ko/guides/web-dashboard.md b/docs-site/src/content/docs/ko/guides/web-dashboard.md index 991991dd272..913157e8036 100644 --- a/docs-site/src/content/docs/ko/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ko/guides/web-dashboard.md @@ -25,7 +25,8 @@ bun run dev:gui `localhost`나 `127.0.0.1` 같은 loopback 주소에서 연 대시보드는 짧게 유지되는 GUI 세션을 자동으로 받으므로 보통 토큰을 입력할 필요가 없습니다. loopback이 아닌 호스트로 공개한 대시보드에는 `OPENCODEX_ADMIN_AUTH_TOKEN` 또는 자동 생성되는 `~/.opencodex/admin-api-token` 파일의 관리자 토큰이 필요합니다. -원격 대시보드는 표준 비밀번호 폼을 표시하므로 브라우저 비밀번호 관리자가 토큰 저장과 자동 완성을 제안할 수 있습니다. 대시보드 자체는 토큰을 메모리에만 보관하며 `localStorage`나 `sessionStorage`에 쓰지 않습니다. 저장 여부는 전적으로 브라우저 또는 비밀번호 관리자가 결정합니다. +원격 대시보드는 표준 비밀번호 폼을 표시하므로 브라우저 비밀번호 관리자가 토큰 저장과 자동 완성을 제안할 수 있습니다. 기본적으로 대시보드는 토큰을 메모리에만 보관합니다. **이 기기에서 기억하기**를 선택하면 전체 토큰을 평문으로 `localStorage`에 저장하는 데 동의하게 됩니다. 동일 출처 스크립트와 이 기기에 접근할 수 있는 사람은 누구나 읽을 수 있으므로 공유 기기에서는 사용하지 마세요. 로그아웃 옆의 **저장된 관리자 토큰 삭제**로 이 값을 지울 수 있습니다. +기억된 값은 서버별로 저장되며, 서버 origin과 전송 방식(transport)으로 구분되고, 저장한 서버에만 다시 전송됩니다. ## 사용량 요약 바 diff --git a/docs-site/src/content/docs/ru/guides/web-dashboard.md b/docs-site/src/content/docs/ru/guides/web-dashboard.md index 7e164d5def6..6eecc6dda5d 100644 --- a/docs-site/src/content/docs/ru/guides/web-dashboard.md +++ b/docs-site/src/content/docs/ru/guides/web-dashboard.md @@ -25,7 +25,8 @@ bun run dev:gui При открытии дашборда через loopback-адрес, например `localhost` или `127.0.0.1`, он автоматически получает краткоживущую GUI-сессию, поэтому ввод токена обычно не требуется. Для дашборда на любом другом хосте нужен административный токен из `OPENCODEX_ADMIN_AUTH_TOKEN` или автоматически созданного файла `~/.opencodex/admin-api-token`. -Удалённый дашборд показывает стандартную форму пароля, поэтому менеджер паролей браузера может предложить сохранить и автозаполнять токен. Сам дашборд хранит токен только в памяти и не записывает его в `localStorage` или `sessionStorage`; решение о сохранении полностью остаётся за браузером или менеджером паролей. +Удалённый дашборд показывает стандартную форму пароля, поэтому менеджер паролей браузера может предложить сохранить и автозаполнять токен. По умолчанию дашборд хранит токен только в памяти. Выбор **Запомнить на этом устройстве** означает согласие на хранение полного токена в открытом виде в `localStorage`. Его может прочитать любой скрипт того же origin и любой, у кого есть доступ к устройству, поэтому не включайте это на общем устройстве. **Удалить сохранённый токен администратора** рядом с выходом удаляет это значение. +Сохранённое значение хранится отдельно для каждого сервера с учётом его origin и транспорта и повторно отправляется только тому серверу, для которого было сохранено. ## Полоса сводки квот diff --git a/docs-site/src/content/docs/tr/guides/web-dashboard.md b/docs-site/src/content/docs/tr/guides/web-dashboard.md index 947da5d9bf7..50f938bdeff 100644 --- a/docs-site/src/content/docs/tr/guides/web-dashboard.md +++ b/docs-site/src/content/docs/tr/guides/web-dashboard.md @@ -34,10 +34,12 @@ oluşturulan `~/.opencodex/admin-api-token` dosyası) gerektirir. Uzak bir kontrol panelinin bu kimlik bilgisine ihtiyacı olduğunda, bir tarayıcı şifre yöneticisinin onu kaydetmeyi ve otomatik doldurmayı teklif edebilmesi için -standart bir şifre formu sunar. Kontrol panelinin kendisi belirteci yine de -yalnızca bellekte tutar ve `localStorage` veya `sessionStorage`'a yazmaz; -kaydedilip kaydedilmeyeceği tamamen tarayıcının veya şifre yöneticisinin -kararıdır. +standart bir şifre formu sunar. Varsayılan olarak kontrol paneli belirteci yalnızca bellekte tutar. +**Bu cihazda hatırla** seçeneği, tam belirteci düz metin olarak `localStorage` içinde saklamayı kabul eder. +Aynı origin'deki herhangi bir betik ve cihaza erişebilen herkes bunu okuyabilir; ortak cihazlarda etkinleştirmeyin. +Çıkışın yanındaki **Kaydedilen yönetici belirtecini unut** bu değeri kaldırır. +Hatırlanan değer sunucu başına, sunucu origin'i ve taşıma türüne göre ayrı saklanır ve +yalnızca kaydedildiği sunucuya yeniden gönderilir. ## Kota özeti çubuğu diff --git a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md index 5b1a432af82..5288b808b18 100644 --- a/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-cn/guides/web-dashboard.md @@ -24,7 +24,8 @@ bun run dev:gui 通过 `localhost`、`127.0.0.1` 等 loopback 地址打开仪表盘时,它会自动获得一个短期 GUI session,因此通常无需输入 token。在非 loopback 主机上公开仪表盘时,必须使用 `OPENCODEX_ADMIN_AUTH_TOKEN` 或自动生成的 `~/.opencodex/admin-api-token` 文件中的管理员 token。 -远程仪表盘会显示标准密码表单,浏览器密码管理器可以提示保存并自动填充 token。仪表盘本身只在内存中保存 token,不会写入 `localStorage` 或 `sessionStorage`;是否持久保存完全由浏览器或密码管理器决定。 +远程仪表盘会显示标准密码表单,浏览器密码管理器可以提示保存并自动填充 token。默认情况下,仪表盘只在内存中保存 token。选择 **在此设备上记住** 即表示同意将完整 token 以明文保存到 `localStorage`。同源脚本以及任何可以访问该设备的人都可以读取它,因此不要在共享设备上启用。登出旁边的 **忘记已保存的管理员令牌** 会删除该值。 +记住的值按服务器分别存储,以服务器 origin 和传输方式区分,只会重新发送到保存它的服务器。 ## 配额摘要栏 diff --git a/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md b/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md index be921cb1efa..b8527ded10d 100644 --- a/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md +++ b/docs-site/src/content/docs/zh-tw/guides/web-dashboard.md @@ -28,8 +28,10 @@ GUI session 簽發到服務的頁面中,並在到期或代理重啟時靜默 `~/.opencodex/admin-api-token` 檔案)。 當遠端儀表板需要該憑證時,它會顯示標準的密碼表單,讓瀏覽器密碼管理員可以提議儲存與自動填入。 -儀表板本身仍然只在記憶體中保留 token,不會寫入 `localStorage` 或 `sessionStorage`;是否儲存完全 -由瀏覽器或密碼管理員決定。 +預設情況下,儀表板只在記憶體中保留 token。選擇 **在此裝置上記住** 即表示同意將完整 token 以純文字 +儲存在 `localStorage`。同源指令碼以及任何可存取該裝置的人都能讀取它,因此不要在共用裝置上啟用。 +登出旁邊的 **忘記已儲存的管理員權杖** 會移除此值。 +記住的值以伺服器為單位儲存,依伺服器 origin 與傳輸方式區分,只會重新傳送給當初儲存它的伺服器。 ## 配額摘要列 diff --git a/gui/src/App.tsx b/gui/src/App.tsx index ef9f1a2b245..5c6eacbf792 100644 --- a/gui/src/App.tsx +++ b/gui/src/App.tsx @@ -16,7 +16,7 @@ import ErrorBoundary from "./components/ErrorBoundary"; import QuotaSummaryBar from "./components/quota-summary-bar/QuotaSummaryBar"; import { SidebarGithubRow } from "./components/sidebar-github-row"; import { DesktopStarOnboarding } from "./components/desktop-star-onboarding"; -import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconCodex, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX, IconRefresh} from "./icons"; +import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconCodex, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX, IconRefresh, IconTrash} from "./icons"; import { useI18n, useT, LOCALES, localeDisplayName, type Locale, type TKey } from "./i18n/shared"; import { Notice, Select, ToastNotice, type NoticeTone } from "./ui"; import { configureApiTargets, hasApiSession, installApiAuthFetch, installApiSessionFromHtml, logoutApiSession, SESSION_UNAVAILABLE_EVENT } from "./api"; @@ -32,6 +32,7 @@ import { hostOs, isDesktopShell, isExternalLink, openDesktopUpdatePage } from ". import { useSidebarCollapse } from "./use-sidebar-collapse"; import { MainTopStrip, SidebarTopStrip } from "./components/app-titlebar"; import { watchMacTitlebarMetrics, windowChromeHandlers } from "./lib/window-chrome"; +import { clearAllRememberedAdminTokens, hasAnyRememberedAdminToken, REMEMBERED_ADMIN_TOKEN_CHANGED_EVENT } from "./admin-token-dialog"; type Theme = "light" | "dark" | "system"; @@ -142,6 +143,7 @@ export default function App() { const [sharedSessionEpoch, setSharedSessionEpoch] = useState(0); const [remoteWorkspaceAvailableState, setRemoteWorkspaceAvailable] = useState(false); const [sessionLoggingOut, setSessionLoggingOut] = useState(false); + const [rememberedAdminTokenPresent, setRememberedAdminTokenPresent] = useState(() => hasAnyRememberedAdminToken()); /* * Results from the two sidebar orbs used to be `alert()`, which the app's webview draws * nowhere, so a refused stop and a completed one looked identical: nothing happened. @@ -165,6 +167,16 @@ export default function App() { return () => window.removeEventListener(SESSION_UNAVAILABLE_EVENT, unavailable); }, []); + useEffect(() => { + const syncRememberedAdminToken = () => setRememberedAdminTokenPresent(hasAnyRememberedAdminToken()); + window.addEventListener(REMEMBERED_ADMIN_TOKEN_CHANGED_EVENT, syncRememberedAdminToken); + window.addEventListener("storage", syncRememberedAdminToken); + return () => { + window.removeEventListener(REMEMBERED_ADMIN_TOKEN_CHANGED_EVENT, syncRememberedAdminToken); + window.removeEventListener("storage", syncRememberedAdminToken); + }; + }, []); + useEffect(() => { const controller = new AbortController(); void discoverApiTargets(API_BASE, controller.signal).then(async next => { @@ -359,6 +371,11 @@ export default function App() { else report(t("connection.sessionLogoutFailed"), "err"); }; + const handleForgetRememberedAdminToken = () => { + clearAllRememberedAdminTokens(); + setRememberedAdminTokenPresent(false); + }; + /* * The brand is the control users reach for first when they want out of a deep page, * and it used to be an inert
: clicking the logo did nothing, so a user on @@ -416,6 +433,12 @@ export default function App() { )} + {rememberedAdminTokenPresent && ( + + )} )} + {rememberedAdminTokenPresent && ( + + )}