Add "Log in with browser" cloud device login (#812)

* Add "Log in with browser" cloud device login

Connects the desktop app to Voicebox Cloud without the user ever handling an
API key. One button in Settings → General opens the system browser to
voicebox.sh, the user authorizes while signed in, and the credential lands
back in the app automatically.

Backend (FastAPI):
- /cloud/login/start opens the browser to the cloud authorize page with a
  state we mint; the existing loopback server catches the redirect at
  /cloud/callback and exchanges the one-time code (server-to-server, over TLS)
  for a voicebox_ API key, verifies it against the API, and stores it.
- /cloud/status and /cloud/disconnect back the settings UI.
- state round-trip guards against login-CSRF; the key never crosses a browser
  URL and is never exposed to the frontend (status returns a prefix only).
- CloudSettings singleton row; config gains VOICEBOX_CLOUD_URL /
  VOICEBOX_CLOUD_API_URL (default the prod hosts, overridable for dev).

Frontend (React):
- CloudSection in Settings → General: "Log in with browser", polls status,
  shows the connected device + a dashboard link. API keys are the advanced
  path only, surfaced in the web dashboard.

The key is stored in the local app DB for now; OS keychain is a marked
follow-up.

* Address review feedback on cloud login

- time out status polling after 2 min so an abandoned browser flow
  doesn't leave the button stuck on "Waiting for browser…"
- handle non-JSON / non-object payloads from the exchange and account
  endpoints instead of 500ing after the state is consumed
- make singleton row creation race-safe (IntegrityError -> re-query)
- clear device_name on disconnect along with the rest of the metadata
- serve the dashboard URL from /cloud/status so the Manage link follows
  VOICEBOX_CLOUD_URL instead of hardcoding production
- keep a "Disconnecting…" label on the disconnect button while pending

* Remove orphaned react-qr-code entries from lockfile

bun.lock was out of date with package.json (react-qr-code was removed
without reinstalling), failing the frozen-lockfile install in CI.
This commit is contained in:
Jamie Pine
2026-07-05 03:18:30 -07:00
committed by GitHub
parent b542768429
commit f2cf2a729d
12 changed files with 505 additions and 5 deletions
+17
View File
@@ -51,6 +51,8 @@ import type {
MCPClientBinding,
MCPClientBindingListResponse,
MCPClientBindingUpsert,
CloudLoginStartResponse,
CloudStatus,
} from './types';
function formatErrorDetail(detail: unknown, fallback: string): string {
@@ -938,6 +940,21 @@ class ApiClient {
return response.blob();
}
// Cloud (backup & sync) — browser-based device login. startCloudLogin opens
// the system browser server-side; the UI then polls getCloudStatus until the
// backend completes the exchange and the link goes live.
async getCloudStatus(): Promise<CloudStatus> {
return this.request<CloudStatus>('/cloud/status');
}
async startCloudLogin(): Promise<CloudLoginStartResponse> {
return this.request<CloudLoginStartResponse>('/cloud/login/start', { method: 'POST' });
}
async disconnectCloud(): Promise<CloudStatus> {
return this.request<CloudStatus>('/cloud/disconnect', { method: 'POST' });
}
}
export const apiClient = new ApiClient();
+15
View File
@@ -542,3 +542,18 @@ export interface MCPClientBindingUpsert {
export interface MCPClientBindingListResponse {
items: MCPClientBinding[];
}
/* ─── Cloud (backup & sync) ───────────────────────────────────────────── */
export interface CloudLoginStartResponse {
authorize_url: string;
}
export interface CloudStatus {
connected: boolean;
device_name: string | null;
account_user_id: string | null;
key_prefix: string | null;
connected_at: string | null;
dashboard_url: string;
}