commit 2 which is probs further from working than 1
This commit is contained in:
@@ -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,19 @@
|
||||
{
|
||||
"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:8080/*"]
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,10 @@
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"security": {
|
||||
"capabilities": ["main"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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';
|
||||
@@ -51,10 +51,54 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
||||
setValidationError(errorMessage);
|
||||
};
|
||||
|
||||
const waitForSsoCompletion = (popup: Window): Promise<string> => {
|
||||
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 (event.origin !== window.location.origin || typeof event.data !== 'object' || event.data === null) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -95,30 +139,9 @@ export const SelfHostedLoginScreen: React.FC<SelfHostedLoginScreenProps> = ({
|
||||
// Mark SSO flow so the callback can short-circuit verification
|
||||
localStorage.setItem('desktop_sso_in_progress', JSON.stringify({ mode: 'selfhosted' }));
|
||||
|
||||
const popup = window.open(provider.url, '_blank', 'width=480,height=720');
|
||||
if (!popup) {
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
handleOAuthError(t('setup.login.error.oauthFailed', 'OAuth login failed. Please try again.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await waitForSsoCompletion(popup);
|
||||
localStorage.removeItem('desktop_sso_in_progress');
|
||||
|
||||
await authService.applyExternalToken(token);
|
||||
await onOAuthSuccess({ username: provider.label || provider.id });
|
||||
} catch (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
|
||||
}
|
||||
}
|
||||
console.debug('[Desktop SSO] Launching provider (same-window)', provider);
|
||||
// Navigate the current webview to the provider URL; the callback page will store the token and reload the app.
|
||||
window.location.assign(provider.url);
|
||||
};
|
||||
|
||||
const displayError = error || validationError;
|
||||
|
||||
Reference in New Issue
Block a user