diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 48a5e1cbd..6ce12fb8f 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -3624,15 +3624,21 @@ async function probeRemoteAuthMode(rawUrl) { let providers = [] if (authRequired) { - // Best-effort: a gated gateway exposes the registered OAuth providers so - // the button can read "Log in with Nous Research" instead of a generic - // label. A failure here doesn't change the auth mode, so swallow it. + // Best-effort: a gated gateway exposes the registered providers so the + // button can read "Sign in with Nous Research" instead of a generic + // label, and so a username/password provider can be distinguished from + // an OAuth-redirect one (``supports_password``). A failure here doesn't + // change the auth mode, so swallow it. try { const body = await fetchPublicJson(`${baseUrl}/api/auth/providers`, { timeoutMs: 8_000 }) if (Array.isArray(body?.providers)) { providers = body.providers .filter(p => p && typeof p === 'object') - .map(p => ({ name: String(p.name || ''), displayName: String(p.display_name || p.name || '') })) + .map(p => ({ + name: String(p.name || ''), + displayName: String(p.display_name || p.name || ''), + supportsPassword: Boolean(p.supports_password) + })) .filter(p => p.name) } } catch { diff --git a/apps/desktop/src/app/settings/gateway-settings.tsx b/apps/desktop/src/app/settings/gateway-settings.tsx index 3fed72458..4bd287e78 100644 --- a/apps/desktop/src/app/settings/gateway-settings.tsx +++ b/apps/desktop/src/app/settings/gateway-settings.tsx @@ -209,6 +209,19 @@ export function GatewaySettings() { return 'your identity provider' }, [probe]) + // A username/password gateway authenticates through a credential form on the + // gateway's /login page (POST /auth/password-login) rather than an OAuth + // redirect. Everything downstream — the session cookie, the ws-ticket mint, + // the persistent partition — is identical, so the desktop drives it through + // the same sign-in window; only the button copy changes. We treat the + // gateway as password-style only when EVERY advertised provider supports + // password, so a mixed deployment keeps the generic OAuth copy. + const isPasswordProvider = useMemo(() => { + const providers: DesktopAuthProvider[] = probe?.providers ?? [] + + return providers.length > 0 && providers.every(p => p.supportsPassword) + }, [probe]) + const oauthConnected = state.remoteOauthConnected const canUseRemote = useMemo(() => { @@ -409,7 +422,7 @@ export function GatewaySettings() { /> setState(current => ({ ...current, mode: 'remote' }))} @@ -446,7 +459,7 @@ export function GatewaySettings() { ) : null} - {/* OAuth gateways: present a sign-in button + connection status. */} + {/* OAuth / password gateways: present a sign-in button + connection status. */} {state.mode === 'remote' && authResolved && authMode === 'oauth' ? ( void signIn()}> {signingIn ? : } - Sign in with {providerLabel} + {isPasswordProvider ? 'Sign in' : `Sign in with ${providerLabel}`} ) } description={ oauthConnected - ? 'This gateway uses OAuth. You are signed in; the session refreshes automatically.' - : `This gateway uses OAuth. Sign in with ${providerLabel} to authorize this desktop app.` + ? isPasswordProvider + ? 'This gateway uses a username and password. You are signed in; the session refreshes automatically.' + : 'This gateway uses OAuth. You are signed in; the session refreshes automatically.' + : isPasswordProvider + ? 'This gateway uses a username and password. Sign in to authorize this desktop app.' + : `This gateway uses OAuth. Sign in with ${providerLabel} to authorize this desktop app.` } title="Authentication" /> diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index f9527c877..e576a9c6e 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -191,6 +191,11 @@ export interface DesktopConnectionTestResult { export interface DesktopAuthProvider { name: string displayName: string + // True when this provider authenticates with a username + password + // (the gateway's /login page renders a credential form) rather than an + // OAuth redirect. The session/cookie/ws-ticket machinery is identical; + // only the login-page form and the desktop's button copy differ. + supportsPassword?: boolean } export interface DesktopConnectionProbeResult {