diff --git a/packages/cli/src/commands/auth/login/command.ts b/packages/cli/src/commands/auth/login/command.ts index 6619ae09e18..4cbfb6bbf31 100644 --- a/packages/cli/src/commands/auth/login/command.ts +++ b/packages/cli/src/commands/auth/login/command.ts @@ -22,7 +22,14 @@ export default command({ spinner?.start("Waiting for browser authorization..."); if (!spinner) p.log.info("Waiting for browser authorization…"); - const result = await login(opts.signal); + const result = await login({ + signal: opts.signal, + onAuthUrl: (url) => { + p.log.info( + `If your browser doesn't open automatically, paste this URL:\n${url}`, + ); + }, + }); config.auth = { accessToken: result.accessToken, diff --git a/packages/cli/src/lib/auth.ts b/packages/cli/src/lib/auth.ts index 5e111592aae..18df944a048 100644 --- a/packages/cli/src/lib/auth.ts +++ b/packages/cli/src/lib/auth.ts @@ -131,7 +131,18 @@ export function getWebUrl(): string { return env.SUPERSET_API_URL.replace("api.superset.sh", "app.superset.sh"); } -export async function login(signal: AbortSignal): Promise { +export interface LoginOptions { + signal: AbortSignal; + /** + * Called once with the authorization URL just before the browser is + * launched. Useful on headless hosts where `xdg-open` isn't available — + * the caller can surface the URL so the user can paste it into a + * browser on a different machine. + */ + onAuthUrl?: (url: string) => void; +} + +export async function login(options: LoginOptions): Promise { const apiUrl = env.SUPERSET_API_URL; const webUrl = getWebUrl(); @@ -143,13 +154,21 @@ export async function login(signal: AbortSignal): Promise { authorizeUrl.searchParams.set("redirect_uri", redirectUri); authorizeUrl.searchParams.set("state", state); - await openBrowser(authorizeUrl.toString()); + const url = authorizeUrl.toString(); + try { + options.onAuthUrl?.(url); + } catch (err) { + // Don't leak the bound loopback server if the callback throws. + server.close(); + throw err; + } + await openBrowser(url); const code = await waitForCallback({ server, port, expectedState: state, - signal, + signal: options.signal, timeoutMs: 5 * 60 * 1000, });