mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92235fd5a9 | ||
|
|
cbf1b387dd | ||
|
|
bbf33a4f60 |
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Stirling PDF - Authentication</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 2rem; color: #111; }
|
||||
.status { padding: 1rem; border-radius: 8px; border: 1px solid #e5e7eb; background: #f9fafb; }
|
||||
.error { border-color: #fca5a5; background: #fef2f2; color: #b91c1c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="message" class="status">Completing authentication...</div>
|
||||
<script>
|
||||
(function () {
|
||||
const msgEl = document.getElementById('message');
|
||||
|
||||
function setStatus(text, isError) {
|
||||
msgEl.textContent = text;
|
||||
if (isError) {
|
||||
msgEl.classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const hash = window.location.hash.startsWith('#') ? window.location.hash.substring(1) : window.location.hash;
|
||||
const params = new URLSearchParams(hash);
|
||||
const token = params.get('access_token');
|
||||
const error = params.get('error_description') || params.get('error');
|
||||
|
||||
// Handle license errors that come via query instead of hash
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const licenseError = urlParams.get('errorOAuth') || urlParams.get('oAuth2RequiresLicense');
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
localStorage.setItem('stirling_jwt', token);
|
||||
window.dispatchEvent(new CustomEvent('jwt-available'));
|
||||
} catch (e) {
|
||||
console.warn('Failed to persist token in localStorage', e);
|
||||
}
|
||||
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'stirling-sso-success', token }, '*');
|
||||
}
|
||||
|
||||
setStatus('Authentication complete. Returning to app...', false);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error || licenseError) {
|
||||
const message = error || 'OAuth/SSO requires a paid license. Please contact your administrator.';
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'stirling-sso-error', error: message }, '*');
|
||||
}
|
||||
setStatus(message, true);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('No authentication token received.', true);
|
||||
} catch (e) {
|
||||
console.error('Auth callback handling error:', e);
|
||||
setStatus('Authentication failed. Please try again.', true);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -51,7 +51,8 @@ class UserLicenseSettingsServiceTest {
|
||||
|
||||
when(applicationProperties.getPremium()).thenReturn(premium);
|
||||
when(applicationProperties.getAutomaticallyGenerated()).thenReturn(automaticallyGenerated);
|
||||
when(automaticallyGenerated.getIsNewServer()).thenReturn(false); // Default: not a new server
|
||||
when(automaticallyGenerated.getIsNewServer())
|
||||
.thenReturn(false); // Default: not a new server
|
||||
when(settingsRepository.findSettings()).thenReturn(Optional.of(mockSettings));
|
||||
when(userService.getTotalUsersCount()).thenReturn(80L);
|
||||
when(settingsRepository.save(any(UserLicenseSettings.class)))
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"identifier": "main",
|
||||
"description": "Main window capability with remote access for self-hosted SSO.",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"http:default",
|
||||
"http:allow-fetch",
|
||||
"http:allow-fetch-send",
|
||||
"http:allow-fetch-read-body",
|
||||
"opener:default",
|
||||
"opener:allow-open-url",
|
||||
"core:webview:allow-internal-toggle-devtools",
|
||||
"core:webview:allow-create-webview-window"
|
||||
],
|
||||
"remote": {
|
||||
"urls": [
|
||||
"http://localhost/*",
|
||||
"http://localhost:*/*",
|
||||
"http://127.0.0.1/*",
|
||||
"http://127.0.0.1:*/*",
|
||||
"http://*/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,10 @@
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"security": {
|
||||
"capabilities": ["main"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -7,12 +7,24 @@ import '@app/routes/authShared/auth.css';
|
||||
|
||||
export type OAuthProvider = 'google' | 'github' | 'keycloak' | 'azure' | 'apple' | 'oidc';
|
||||
|
||||
export interface DesktopOAuthProvider {
|
||||
id: string;
|
||||
label?: string;
|
||||
file?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
interface DesktopOAuthButtonsProps {
|
||||
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
||||
onError: (error: string) => void;
|
||||
isDisabled: boolean;
|
||||
serverUrl: string;
|
||||
providers: OAuthProvider[];
|
||||
providers: DesktopOAuthProvider[];
|
||||
/**
|
||||
* Optional override to handle provider click (used for self-hosted SSO flow).
|
||||
* When provided, the caller is responsible for invoking onOAuthSuccess/onError.
|
||||
*/
|
||||
onProviderClick?: (provider: DesktopOAuthProvider) => Promise<void>;
|
||||
}
|
||||
|
||||
export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
||||
@@ -21,6 +33,7 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
||||
isDisabled,
|
||||
serverUrl,
|
||||
providers,
|
||||
onProviderClick,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [oauthLoading, setOauthLoading] = useState(false);
|
||||
@@ -64,7 +77,25 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const providerConfig: Record<OAuthProvider, { label: string; file: string }> = {
|
||||
const handleCustomClick = async (provider: DesktopOAuthProvider) => {
|
||||
if (!onProviderClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setOauthLoading(true);
|
||||
await onProviderClick(provider);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.');
|
||||
onError(errorMessage);
|
||||
} finally {
|
||||
setOauthLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const providerConfig: Record<string, { label: string; file: string }> = {
|
||||
google: { label: 'Google', file: 'google.svg' },
|
||||
github: { label: 'GitHub', file: 'github.svg' },
|
||||
keycloak: { label: 'Keycloak', file: 'keycloak.svg' },
|
||||
@@ -80,19 +111,31 @@ export const DesktopOAuthButtons: React.FC<DesktopOAuthButtonsProps> = ({
|
||||
return (
|
||||
<div className="oauth-container-vertical">
|
||||
{providers
|
||||
.filter((providerId) => providerId in providerConfig)
|
||||
.map((providerId) => {
|
||||
const provider = providerConfig[providerId];
|
||||
.map((providerMeta) => {
|
||||
const provider =
|
||||
(providerMeta.id in providerConfig
|
||||
? providerConfig[providerMeta.id as OAuthProvider]
|
||||
: {
|
||||
label: providerMeta.label || providerMeta.id,
|
||||
file: providerMeta.file || 'oidc.svg',
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
key={providerId}
|
||||
onClick={() => handleOAuthLogin(providerId)}
|
||||
key={providerMeta.id}
|
||||
onClick={() => {
|
||||
if (onProviderClick) {
|
||||
void handleCustomClick(providerMeta);
|
||||
} else {
|
||||
void handleOAuthLogin(providerMeta.id as OAuthProvider);
|
||||
}
|
||||
}}
|
||||
disabled={isDisabled || oauthLoading}
|
||||
className="oauth-button-vertical"
|
||||
title={provider.label}
|
||||
>
|
||||
<img
|
||||
src={`${BASE_PATH}/Login/${provider.file}`}
|
||||
src={`${BASE_PATH}/Login/${provider.file || 'oidc.svg'}`}
|
||||
alt={provider.label}
|
||||
className="oauth-icon-tiny"
|
||||
/>
|
||||
|
||||
@@ -64,7 +64,10 @@ export const SaaSLoginScreen: React.FC<SaaSLoginScreenProps> = ({
|
||||
onError={handleOAuthError}
|
||||
isDisabled={loading}
|
||||
serverUrl={serverUrl}
|
||||
providers={['google', 'github']}
|
||||
providers={[
|
||||
{ id: 'google' },
|
||||
{ id: 'github' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<DividerWithText
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Text } from '@mantine/core';
|
||||
import LoginHeader from '@app/routes/login/LoginHeader';
|
||||
import ErrorMessage from '@app/routes/login/ErrorMessage';
|
||||
import EmailPasswordForm from '@app/routes/login/EmailPasswordForm';
|
||||
import DividerWithText from '@app/components/shared/DividerWithText';
|
||||
import { DesktopOAuthButtons, OAuthProvider } from '@app/components/SetupWizard/DesktopOAuthButtons';
|
||||
import { UserInfo } from '@app/services/authService';
|
||||
import { DesktopOAuthButtons, DesktopOAuthProvider } from '@app/components/SetupWizard/DesktopOAuthButtons';
|
||||
import { authService, UserInfo } from '@app/services/authService';
|
||||
import '@app/routes/authShared/auth.css';
|
||||
|
||||
interface SelfHostedLoginScreenProps {
|
||||
serverUrl: string;
|
||||
enabledOAuthProviders?: string[];
|
||||
enabledOAuthProviders?: DesktopOAuthProvider[];
|
||||
onLogin: (username: string, password: string) => Promise<void>;
|
||||
onOAuthSuccess: (userInfo: UserInfo) => Promise<void>;
|
||||
loading: boolean;
|
||||
@@ -51,6 +51,124 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
||||
setValidationError(errorMessage);
|
||||
};
|
||||
|
||||
const pollForSession = (timeoutMs = 60000): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = Date.now();
|
||||
const interval = window.setInterval(async () => {
|
||||
if (Date.now() - start > timeoutMs) {
|
||||
window.clearInterval(interval);
|
||||
reject(new Error('Timed out waiting for authentication'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const hasToken = localStorage.getItem('stirling_jwt');
|
||||
if (hasToken) {
|
||||
window.clearInterval(interval);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const isAuthed = await authService.isAuthenticated();
|
||||
if (isAuthed) {
|
||||
window.clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore transient errors and keep polling
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
const waitForSsoCompletion = (popup: Window, expectedOrigin?: string): Promise<string> => {
|
||||
// Accept messages from the backend origin (e.g., http://localhost:8080) since the wizard runs under the Tauri origin.
|
||||
const allowedOrigin = (() => {
|
||||
try {
|
||||
return expectedOrigin ? new URL(expectedOrigin).origin : null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageHandler = (event: MessageEvent) => {
|
||||
if (typeof event.data !== 'object' || event.data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we can compute the backend origin, ensure the message comes from there.
|
||||
if (allowedOrigin && event.origin !== allowedOrigin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, token, error } = event.data as { type?: string; token?: string; error?: string };
|
||||
if (type === 'stirling-sso-success' && token) {
|
||||
cleanup();
|
||||
resolve(token);
|
||||
} else if (type === 'stirling-sso-error') {
|
||||
cleanup();
|
||||
reject(new Error(error || 'SSO login failed'));
|
||||
}
|
||||
};
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
if (popup.closed) {
|
||||
cleanup();
|
||||
reject(new Error('Login window was closed before authentication completed'));
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const cleanup = () => {
|
||||
window.clearInterval(interval);
|
||||
window.removeEventListener('message', messageHandler);
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelfHostedOAuthLogin = async (provider: DesktopOAuthProvider) => {
|
||||
setValidationError(null);
|
||||
|
||||
if (!provider.url) {
|
||||
handleOAuthError(t('setup.login.error.configFetch', 'Failed to fetch server configuration. Please check the URL and try again.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark SSO flow so the callback can short-circuit verification
|
||||
localStorage.setItem('desktop_sso_in_progress', JSON.stringify({ mode: 'selfhosted' }));
|
||||
|
||||
console.debug('[Desktop SSO] Launching provider (popup)', provider);
|
||||
const popup = window.open(provider.url, '_blank', 'width=520,height=720');
|
||||
|
||||
if (!popup) {
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
console.error('[Desktop SSO] Failed to open popup window for provider', provider);
|
||||
handleOAuthError(t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await waitForSsoCompletion(popup, serverUrl);
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
|
||||
await authService.applyExternalToken(token);
|
||||
await onOAuthSuccess({ username: provider.label || provider.id });
|
||||
} catch (err) {
|
||||
console.error('[Desktop SSO] OAuth flow failed for provider', provider, err);
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
const message = err instanceof Error ? err.message : t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.');
|
||||
handleOAuthError(message);
|
||||
} finally {
|
||||
try {
|
||||
popup.close();
|
||||
} catch (_) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const displayError = error || validationError;
|
||||
|
||||
return (
|
||||
@@ -74,7 +192,8 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
||||
onError={handleOAuthError}
|
||||
isDisabled={loading}
|
||||
serverUrl={serverUrl}
|
||||
providers={enabledOAuthProviders as OAuthProvider[]}
|
||||
providers={enabledOAuthProviders}
|
||||
onProviderClick={handleSelfHostedOAuthLogin}
|
||||
/>
|
||||
|
||||
<DividerWithText
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { ServerConfig } from '@app/services/connectionModeService';
|
||||
import { connectionModeService } from '@app/services/connectionModeService';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
import { DesktopOAuthProvider } from '@app/components/SetupWizard/DesktopOAuthButtons';
|
||||
|
||||
interface ServerSelectionProps {
|
||||
onSelect: (config: ServerConfig) => void;
|
||||
@@ -43,7 +44,7 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
|
||||
}
|
||||
|
||||
// Fetch OAuth providers and check if login is enabled
|
||||
let enabledProviders: string[] = [];
|
||||
let enabledProviders: DesktopOAuthProvider[] = [];
|
||||
try {
|
||||
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`);
|
||||
|
||||
@@ -74,11 +75,19 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract provider IDs from authorization URLs
|
||||
// Example: "/oauth2/authorization/google" → "google"
|
||||
enabledProviders = Object.keys(data.providerList || {})
|
||||
.map(key => key.split('/').pop())
|
||||
.filter((id): id is string => id !== undefined);
|
||||
// Extract provider metadata from authorization URLs
|
||||
// Example: "/oauth2/authorization/google" → { id: "google", url: "https://server/oauth2/authorization/google" }
|
||||
enabledProviders = Object.entries(data.providerList || {})
|
||||
.map(([path, label]) => {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const id = normalizedPath.split('/').pop() || normalizedPath;
|
||||
const trimmedUrl = url.replace(/\/$/, '');
|
||||
return {
|
||||
id,
|
||||
label: typeof label === 'string' ? label : undefined,
|
||||
url: `${trimmedUrl}${normalizedPath}`,
|
||||
} satisfies DesktopOAuthProvider;
|
||||
});
|
||||
|
||||
console.log('[ServerSelection] Detected OAuth providers:', enabledProviders);
|
||||
} catch (err) {
|
||||
|
||||
@@ -69,6 +69,11 @@ export class AuthService {
|
||||
// Notify other parts of the system
|
||||
window.dispatchEvent(new CustomEvent('jwt-available'));
|
||||
console.log('[Desktop AuthService] Dispatched jwt-available event');
|
||||
|
||||
// Keep auth status in sync if we already have user info
|
||||
if (this.userInfo) {
|
||||
this.setAuthStatus('authenticated', this.userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,6 +105,19 @@ export class AuthService {
|
||||
return localStorageToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a JWT obtained from an external flow (e.g., browser SSO).
|
||||
*/
|
||||
async applyExternalToken(token: string, userInfo?: UserInfo | null): Promise<void> {
|
||||
await this.saveTokenEverywhere(token);
|
||||
|
||||
if (userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
}
|
||||
|
||||
this.setAuthStatus('authenticated', userInfo ?? this.userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear token from all storage locations
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,11 @@ export type ConnectionMode = 'saas' | 'selfhosted';
|
||||
|
||||
export interface ServerConfig {
|
||||
url: string;
|
||||
enabledOAuthProviders?: string[];
|
||||
enabledOAuthProviders?: Array<{
|
||||
id: string;
|
||||
label?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ConnectionConfig {
|
||||
@@ -104,13 +108,22 @@ export class ConnectionModeService {
|
||||
try {
|
||||
// Test connection by hitting the health/status endpoint
|
||||
const healthUrl = `${url.replace(/\/$/, '')}/api/v1/info/status`;
|
||||
|
||||
// Prefer the browser fetch to avoid Tauri HTTP permission blockers
|
||||
if (typeof window !== 'undefined' && window.fetch) {
|
||||
const response = await window.fetch(healthUrl, { method: 'GET' });
|
||||
const isOk = response.ok;
|
||||
console.log(`[ConnectionModeService] Server connection test result (browser fetch): ${isOk}`);
|
||||
return isOk;
|
||||
}
|
||||
|
||||
// Fallback to Tauri HTTP plugin
|
||||
const response = await fetch(healthUrl, {
|
||||
method: 'GET',
|
||||
connectTimeout: 10000,
|
||||
});
|
||||
|
||||
const isOk = response.ok;
|
||||
console.log(`[ConnectionModeService] Server connection test result: ${isOk}`);
|
||||
console.log(`[ConnectionModeService] Server connection test result (tauri fetch): ${isOk}`);
|
||||
return isOk;
|
||||
} catch (error) {
|
||||
console.warn('[ConnectionModeService] Server connection test failed:', error);
|
||||
|
||||
@@ -21,9 +21,20 @@ export default function AuthCallback() {
|
||||
const hash = window.location.hash.substring(1); // Remove '#'
|
||||
const params = new URLSearchParams(hash);
|
||||
const token = params.get('access_token');
|
||||
const desktopSsoState = localStorage.getItem('desktop_sso_in_progress');
|
||||
const isDesktopSso = Boolean(desktopSsoState);
|
||||
|
||||
if (!token) {
|
||||
console.error('[AuthCallback] No access_token in URL fragment');
|
||||
if (isDesktopSso && window.opener) {
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
window.opener.postMessage(
|
||||
{ type: 'stirling-sso-error', error: 'OAuth login failed - no token received.' },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
navigate('/login', {
|
||||
replace: true,
|
||||
state: { error: 'OAuth login failed - no token received.' }
|
||||
@@ -38,26 +49,53 @@ export default function AuthCallback() {
|
||||
// Dispatch custom event for other components to react to JWT availability
|
||||
window.dispatchEvent(new CustomEvent('jwt-available'));
|
||||
|
||||
// Validate the token and load user info
|
||||
// This calls /api/v1/auth/me with the JWT to get user details
|
||||
const { data, error } = await springAuth.getSession();
|
||||
// Desktop SSO flow relies on the opener to finalize setup, so skip server validation here
|
||||
if (!isDesktopSso) {
|
||||
// Validate the token and load user info
|
||||
// This calls /api/v1/auth/me with the JWT to get user details
|
||||
const { data, error } = await springAuth.getSession();
|
||||
|
||||
if (error || !data.session) {
|
||||
console.error('[AuthCallback] Failed to validate token:', error);
|
||||
localStorage.removeItem('stirling_jwt');
|
||||
navigate('/login', {
|
||||
replace: true,
|
||||
state: { error: 'OAuth login failed - invalid token.' }
|
||||
});
|
||||
return;
|
||||
if (error || !data.session) {
|
||||
console.error('[AuthCallback] Failed to validate token:', error);
|
||||
localStorage.removeItem('stirling_jwt');
|
||||
navigate('/login', {
|
||||
replace: true,
|
||||
state: { error: 'OAuth login failed - invalid token.' }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup flag for desktop flow
|
||||
if (isDesktopSso) {
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
}
|
||||
|
||||
console.log('[AuthCallback] Token validated, redirecting to home');
|
||||
|
||||
if (isDesktopSso && window.opener) {
|
||||
window.opener.postMessage(
|
||||
{ type: 'stirling-sso-success', token },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the hash from URL and redirect to home page
|
||||
navigate('/', { replace: true });
|
||||
} catch (error) {
|
||||
console.error('[AuthCallback] Error:', error);
|
||||
const desktopSsoState = localStorage.getItem('desktop_sso_in_progress');
|
||||
if (desktopSsoState && window.opener) {
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
window.opener.postMessage(
|
||||
{ type: 'stirling-sso-error', error: 'OAuth login failed. Please try again.' },
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
navigate('/login', {
|
||||
replace: true,
|
||||
state: { error: 'OAuth login failed. Please try again.' }
|
||||
|
||||
Reference in New Issue
Block a user