From 375cf0ede115426dfa006b2542ef99f9a97ff0d3 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 18 Dec 2025 09:06:58 +0000 Subject: [PATCH] First complete OAuth self-hosted flow --- .../web/ReactRoutingController.java | 4 +- frontend/src/desktop/services/authService.ts | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 20f004eea..b65752f5e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -176,7 +176,7 @@ public class ReactRoutingController { const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, '')); const searchParams = new URLSearchParams(window.location.search); const token = hashParams.get('access_token') || hashParams.get('token') || searchParams.get('access_token'); - const isDesktopPopup = window.opener && window.name === 'stirling-desktop-sso'; + const isDesktopPopup = !!window.opener; const serverUrl = %s; if (token) { @@ -226,7 +226,7 @@ public class ReactRoutingController { const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, '')); const searchParams = new URLSearchParams(window.location.search); const token = hashParams.get('access_token') || hashParams.get('token') || searchParams.get('access_token'); - const isDesktopPopup = window.opener && window.name === 'stirling-desktop-sso'; + const isDesktopPopup = !!window.opener; const serverUrl = %s; if (token) { diff --git a/frontend/src/desktop/services/authService.ts b/frontend/src/desktop/services/authService.ts index 11d408e61..51b3ebfd0 100644 --- a/frontend/src/desktop/services/authService.ts +++ b/frontend/src/desktop/services/authService.ts @@ -1,6 +1,8 @@ import { invoke, isTauri } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { open as shellOpen } from '@tauri-apps/plugin-shell'; +import { connectionModeService } from '@app/services/connectionModeService'; +import { tauriBackendService } from '@app/services/tauriBackendService'; import axios from 'axios'; import { DESKTOP_DEEP_LINK_CALLBACK, STIRLING_SAAS_URL, SUPABASE_KEY } from '@app/constants/connection'; @@ -457,7 +459,7 @@ export class AuthService { } // Force a real popup so the main webview stays on the app - const authWindow = window.open(fullUrl, '_blank', 'width=900,height=900'); + const authWindow = window.open(fullUrl, 'stirling-desktop-sso', 'width=900,height=900'); // Fallback: use Tauri shell.open and wait for deep link back if (!authWindow) { @@ -470,7 +472,10 @@ export class AuthService { const expectedOrigin = new URL(fullUrl).origin; // Always also listen for deep link completion in case the opener messaging path fails - const deepLinkPromise = this.waitForDeepLinkCompletion(trimmedServer).catch(() => null); + const deepLinkPromise = this.waitForDeepLinkCompletion(trimmedServer).catch((err) => { + console.warn('[Desktop AuthService] Deep link completion failed or timed out:', err); + return null; + }); return new Promise((resolve, reject) => { let completed = false; @@ -590,6 +595,28 @@ export class AuthService { } }, 120_000); + const localPollId = window.setInterval(async () => { + if (completed) { + window.clearInterval(localPollId); + return; + } + const token = localStorage.getItem('stirling_jwt'); + if (token) { + completed = true; + window.clearInterval(localPollId); + if (unlisten) unlisten(); + clearTimeout(timeoutId); + try { + const userInfo = await this.completeSelfHostedSession(serverUrl, token); + await connectionModeService.switchToSelfHosted({ url: serverUrl }); + await tauriBackendService.initializeExternalBackend(); + resolve(userInfo); + } catch (err) { + reject(err instanceof Error ? err : new Error('Failed to complete SSO')); + } + } + }, 1000); + listen('deep-link', async (event) => { const url = event.payload; if (!url || completed) return; @@ -609,13 +636,22 @@ export class AuthService { completed = true; if (unlisten) unlisten(); clearTimeout(timeoutId); + window.clearInterval(localPollId); const userInfo = await this.completeSelfHostedSession(serverUrl, token); + // Ensure connection mode is set and backend is ready (in case caller doesn't) + try { + await connectionModeService.switchToSelfHosted({ url: serverUrl }); + await tauriBackendService.initializeExternalBackend(); + } catch (e) { + console.warn('[Desktop AuthService] Failed to initialize backend after deep link:', e); + } resolve(userInfo); } catch (err) { completed = true; if (unlisten) unlisten(); clearTimeout(timeoutId); + window.clearInterval(localPollId); reject(err instanceof Error ? err : new Error('Failed to complete SSO')); } }).then((fn) => {