mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Self-hosted desktop SSO (#5265)
# Description of Changes Support SSO in self-hosted desktop app. --------- Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
co-authored by
Anthony Stirling
parent
dd09f7b7cf
commit
18be8f4692
@@ -141,10 +141,10 @@ public class SPDFApplication {
|
||||
String backendUrl = appConfig.getBackendUrl();
|
||||
String contextPath = appConfig.getContextPath();
|
||||
String serverPort = appConfig.getServerPort();
|
||||
baseUrlStatic = backendUrl;
|
||||
baseUrlStatic = normalizeBackendUrl(backendUrl, serverPort);
|
||||
contextPathStatic = contextPath;
|
||||
serverPortStatic = serverPort;
|
||||
String url = backendUrl + ":" + getStaticPort() + contextPath;
|
||||
String url = buildFullUrl(baseUrlStatic, getStaticPort(), contextPathStatic);
|
||||
|
||||
// Log Tauri mode information
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_TAURI_MODE", "false"))) {
|
||||
@@ -210,7 +210,7 @@ public class SPDFApplication {
|
||||
|
||||
private static void printStartupLogs() {
|
||||
log.info("Stirling-PDF Started.");
|
||||
String url = baseUrlStatic + ":" + getStaticPort() + contextPathStatic;
|
||||
String url = buildFullUrl(baseUrlStatic, getStaticPort(), contextPathStatic);
|
||||
log.info("Navigate to {}", url);
|
||||
}
|
||||
|
||||
@@ -258,4 +258,79 @@ public class SPDFApplication {
|
||||
public static String getStaticContextPath() {
|
||||
return contextPathStatic;
|
||||
}
|
||||
|
||||
private static String buildFullUrl(String backendUrl, String port, String contextPath) {
|
||||
String normalizedBase = normalizeBackendUrl(backendUrl, port);
|
||||
|
||||
String normalizedContextPath =
|
||||
(contextPath == null || contextPath.isBlank() || "/".equals(contextPath))
|
||||
? "/"
|
||||
: (contextPath.startsWith("/") ? contextPath : "/" + contextPath);
|
||||
|
||||
return normalizedBase + normalizedContextPath;
|
||||
}
|
||||
|
||||
private static String normalizeBackendUrl(String backendUrl, String port) {
|
||||
String trimmedBase =
|
||||
(backendUrl == null || backendUrl.isBlank())
|
||||
? "http://localhost"
|
||||
: backendUrl.trim().replaceAll("/+$", "");
|
||||
boolean hasScheme = trimmedBase.matches("^[a-zA-Z][a-zA-Z0-9+.-]*://.*");
|
||||
String baseForParsing = hasScheme ? trimmedBase : "http://" + trimmedBase;
|
||||
Integer parsedPort = parsePort(port);
|
||||
|
||||
try {
|
||||
java.net.URI uri = new java.net.URI(baseForParsing);
|
||||
String scheme = uri.getScheme() == null ? "http" : uri.getScheme();
|
||||
String host = uri.getHost();
|
||||
if (host == null) {
|
||||
return appendPortFallback(trimmedBase, parsedPort);
|
||||
}
|
||||
|
||||
boolean defaultHttp =
|
||||
"http".equalsIgnoreCase(scheme) && Integer.valueOf(80).equals(parsedPort);
|
||||
boolean defaultHttps =
|
||||
"https".equalsIgnoreCase(scheme) && Integer.valueOf(443).equals(parsedPort);
|
||||
|
||||
int effectivePort = uri.getPort();
|
||||
if (effectivePort == -1 && parsedPort != null && !defaultHttp && !defaultHttps) {
|
||||
effectivePort = parsedPort;
|
||||
}
|
||||
|
||||
java.net.URI rebuilt =
|
||||
new java.net.URI(
|
||||
scheme,
|
||||
uri.getUserInfo(),
|
||||
host,
|
||||
effectivePort,
|
||||
uri.getPath(),
|
||||
uri.getQuery(),
|
||||
uri.getFragment());
|
||||
return rebuilt.toString();
|
||||
} catch (java.net.URISyntaxException e) {
|
||||
return appendPortFallback(trimmedBase, parsedPort);
|
||||
}
|
||||
}
|
||||
|
||||
private static Integer parsePort(String port) {
|
||||
if (port == null || port.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int parsed = Integer.parseInt(port);
|
||||
return parsed > 0 ? parsed : null;
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String appendPortFallback(String trimmedBase, Integer port) {
|
||||
if (port == null) {
|
||||
return trimmedBase;
|
||||
}
|
||||
if (trimmedBase.matches(".+:\\d+$")) {
|
||||
return trimmedBase;
|
||||
}
|
||||
return trimmedBase + ":" + port;
|
||||
}
|
||||
}
|
||||
|
||||
+392
-49
@@ -15,84 +15,106 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
import org.springframework.web.util.JavaScriptUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class ReactRoutingController {
|
||||
|
||||
private static final org.slf4j.Logger log =
|
||||
org.slf4j.LoggerFactory.getLogger(ReactRoutingController.class);
|
||||
|
||||
@Value("${server.servlet.context-path:/}")
|
||||
private String contextPath;
|
||||
|
||||
private String cachedIndexHtml;
|
||||
private String cachedCallbackHtml;
|
||||
private boolean indexHtmlExists = false;
|
||||
private boolean useExternalIndexHtml = false;
|
||||
private boolean loggedMissingIndex = false;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
log.info("Static files custom path: {}", InstallationPathConfig.getStaticPath());
|
||||
|
||||
// Always initialize callback HTML (used for OAuth desktop flow)
|
||||
this.cachedCallbackHtml = buildCallbackHtml();
|
||||
|
||||
// Check for external index.html first (customFiles/static/)
|
||||
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
|
||||
log.debug("Checking for custom index.html at: {}", externalIndexPath);
|
||||
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
|
||||
log.info("Using custom index.html from: {}", externalIndexPath);
|
||||
try {
|
||||
this.cachedIndexHtml = processIndexHtml();
|
||||
this.indexHtmlExists = true;
|
||||
this.useExternalIndexHtml = true;
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to load custom index.html, falling back to classpath", e);
|
||||
}
|
||||
this.cachedIndexHtml = processIndexHtml();
|
||||
this.indexHtmlExists = true;
|
||||
this.useExternalIndexHtml = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to classpath index.html
|
||||
ClassPathResource resource = new ClassPathResource("static/index.html");
|
||||
if (resource.exists()) {
|
||||
try {
|
||||
this.cachedIndexHtml = processIndexHtml();
|
||||
this.indexHtmlExists = true;
|
||||
this.useExternalIndexHtml = false;
|
||||
} catch (IOException e) {
|
||||
// Failed to cache, will process on each request
|
||||
log.warn("Failed to cache index.html", e);
|
||||
this.indexHtmlExists = false;
|
||||
this.cachedIndexHtml = processIndexHtml();
|
||||
this.indexHtmlExists = true;
|
||||
this.useExternalIndexHtml = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Neither external nor classpath index.html exists - cache fallback once
|
||||
this.cachedIndexHtml = buildFallbackHtml();
|
||||
this.indexHtmlExists = true;
|
||||
this.useExternalIndexHtml = false;
|
||||
this.loggedMissingIndex = true;
|
||||
log.warn(
|
||||
"index.html not found in classpath or custom path; using lightweight fallback page");
|
||||
}
|
||||
|
||||
private String processIndexHtml() {
|
||||
try {
|
||||
Resource resource = getIndexHtmlResource();
|
||||
|
||||
if (!resource.exists()) {
|
||||
if (!loggedMissingIndex) {
|
||||
log.warn("index.html not found, using lightweight fallback page");
|
||||
loggedMissingIndex = true;
|
||||
}
|
||||
return buildFallbackHtml();
|
||||
}
|
||||
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
// Replace %BASE_URL% with the actual context path for base href
|
||||
String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/";
|
||||
html = html.replace("%BASE_URL%", baseUrl);
|
||||
// Also rewrite any existing <base> tag (Vite may have baked one in)
|
||||
html =
|
||||
html.replaceFirst(
|
||||
"<base href=\\\"[^\\\"]*\\\"\\s*/?>",
|
||||
"<base href=\\\"" + baseUrl + "\\\" />");
|
||||
|
||||
// Inject context path as a global variable for API calls
|
||||
String contextPathScript =
|
||||
"<script>window.STIRLING_PDF_API_BASE_URL = '" + baseUrl + "';</script>";
|
||||
html = html.replace("</head>", contextPathScript + "</head>");
|
||||
|
||||
return html;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
if (!loggedMissingIndex) {
|
||||
log.warn("index.html not found, using lightweight fallback page", ex);
|
||||
loggedMissingIndex = true;
|
||||
}
|
||||
return buildFallbackHtml();
|
||||
}
|
||||
}
|
||||
|
||||
private String processIndexHtml() throws IOException {
|
||||
Resource resource = getIndexHtmlResource();
|
||||
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
String html = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
||||
// Replace %BASE_URL% with the actual context path for base href
|
||||
String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/";
|
||||
html = html.replace("%BASE_URL%", baseUrl);
|
||||
// Also rewrite any existing <base> tag (Vite may have baked one in)
|
||||
html =
|
||||
html.replaceFirst(
|
||||
"<base href=\\\"[^\\\"]*\\\"\\s*/?>",
|
||||
"<base href=\\\"" + baseUrl + "\\\" />");
|
||||
|
||||
// Inject context path as a global variable for API calls
|
||||
String contextPathScript =
|
||||
"<script>window.STIRLING_PDF_API_BASE_URL = '" + baseUrl + "';</script>";
|
||||
html = html.replace("</head>", contextPathScript + "</head>");
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
private Resource getIndexHtmlResource() throws IOException {
|
||||
private Resource getIndexHtmlResource() {
|
||||
// Check external location first
|
||||
Path externalIndexPath = Paths.get(InstallationPathConfig.getStaticPath(), "index.html");
|
||||
if (Files.exists(externalIndexPath) && Files.isReadable(externalIndexPath)) {
|
||||
@@ -106,12 +128,28 @@ public class ReactRoutingController {
|
||||
@GetMapping(
|
||||
value = {"/", "/index.html"},
|
||||
produces = MediaType.TEXT_HTML_VALUE)
|
||||
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request) throws IOException {
|
||||
if (indexHtmlExists && cachedIndexHtml != null) {
|
||||
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(cachedIndexHtml);
|
||||
public ResponseEntity<String> serveIndexHtml(HttpServletRequest request) {
|
||||
try {
|
||||
if (indexHtmlExists && cachedIndexHtml != null) {
|
||||
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(cachedIndexHtml);
|
||||
}
|
||||
// Fallback: process on each request (dev mode or cache failed)
|
||||
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(processIndexHtml());
|
||||
} catch (Exception ex) {
|
||||
log.error("Failed to serve index.html, returning fallback", ex);
|
||||
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(buildFallbackHtml());
|
||||
}
|
||||
// Fallback: process on each request (dev mode or cache failed)
|
||||
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(processIndexHtml());
|
||||
}
|
||||
|
||||
@GetMapping(value = "/auth/callback", produces = MediaType.TEXT_HTML_VALUE)
|
||||
public ResponseEntity<String> serveAuthCallback(HttpServletRequest request) {
|
||||
return serveIndexHtml(request);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/auth/callback/tauri", produces = MediaType.TEXT_HTML_VALUE)
|
||||
public ResponseEntity<String> serveTauriAuthCallback(HttpServletRequest request) {
|
||||
// cachedCallbackHtml is always initialized in @PostConstruct
|
||||
return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(cachedCallbackHtml);
|
||||
}
|
||||
|
||||
@GetMapping(
|
||||
@@ -126,4 +164,309 @@ public class ReactRoutingController {
|
||||
throws IOException {
|
||||
return serveIndexHtml(request);
|
||||
}
|
||||
|
||||
private String buildFallbackHtml() {
|
||||
String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/";
|
||||
|
||||
// Escape for HTML attribute context
|
||||
String escapedBaseUrlHtml = HtmlUtils.htmlEscape(baseUrl);
|
||||
|
||||
// Escape for JavaScript string context
|
||||
String escapedBaseUrlJs = JavaScriptUtils.javaScriptEscape(baseUrl);
|
||||
|
||||
String serverUrl = "(window.location.origin + '" + escapedBaseUrlJs + "')";
|
||||
return """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="%s" />
|
||||
<title>Stirling PDF</title>
|
||||
<script>
|
||||
// Minimal handler for SSO callback when index.html is missing (desktop fallback)
|
||||
(function() {
|
||||
const baseUrl = '%s';
|
||||
window.STIRLING_PDF_API_BASE_URL = baseUrl;
|
||||
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 serverUrl = %s;
|
||||
|
||||
if (token) {
|
||||
// Extract nonce from URL to send back to desktop app for validation
|
||||
const nonceFromUrl = hashParams.get('nonce') || searchParams.get('nonce');
|
||||
|
||||
console.log('[Fallback Auth] Token received, sending to desktop app via deep link');
|
||||
|
||||
// Send token + nonce via deep link to desktop app
|
||||
// Desktop app will validate nonce before accepting token
|
||||
try {
|
||||
const encodedToken = encodeURIComponent(token);
|
||||
const encodedServer = encodeURIComponent(serverUrl);
|
||||
const encodedNonce = nonceFromUrl ? encodeURIComponent(nonceFromUrl) : '';
|
||||
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#access_token=${encodedToken}&nonce=${encodedNonce}&type=sso-selfhosted`;
|
||||
window.location.href = deepLink;
|
||||
return;
|
||||
} catch (_) {
|
||||
// ignore deep link errors
|
||||
}
|
||||
}
|
||||
|
||||
// No redirect to avoid loops when index.html is missing
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>Stirling PDF is running.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
.formatted(escapedBaseUrlHtml, escapedBaseUrlJs, serverUrl);
|
||||
}
|
||||
|
||||
private String buildCallbackHtml() {
|
||||
String baseUrl = contextPath.endsWith("/") ? contextPath : contextPath + "/";
|
||||
|
||||
// Escape for HTML attribute context
|
||||
String escapedBaseUrlHtml = HtmlUtils.htmlEscape(baseUrl);
|
||||
|
||||
// Escape for JavaScript string context
|
||||
String escapedBaseUrlJs = JavaScriptUtils.javaScriptEscape(baseUrl);
|
||||
|
||||
String serverUrl = "(window.location.origin + '" + escapedBaseUrlJs + "')";
|
||||
return """
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="%s" />
|
||||
<title>Authentication Complete</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 50px 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
max-width: 420px;
|
||||
width: 100%%;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.icon.error {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
background: #ffebee;
|
||||
border: 1px solid #ffcdd2;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
color: #c62828;
|
||||
word-break: break-word;
|
||||
text-align: left;
|
||||
line-height: 1.5;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: #2d2d2d;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
border-color: #374151;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: #66bb6a;
|
||||
}
|
||||
|
||||
.icon.error {
|
||||
color: #ef5350;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.error-details {
|
||||
background: #3d2020;
|
||||
border: 1px solid #5d3030;
|
||||
color: #ef9a9a;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function() {
|
||||
const run = () => {
|
||||
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 errorCode = searchParams.get('errorOAuth')
|
||||
|| searchParams.get('error')
|
||||
|| hashParams.get('error')
|
||||
|| searchParams.get('error_description')
|
||||
|| hashParams.get('error_description');
|
||||
const serverUrl = %s;
|
||||
const iconEl = document.getElementById('auth-icon');
|
||||
const titleEl = document.getElementById('auth-title');
|
||||
const messageEl = document.getElementById('auth-message');
|
||||
const detailsEl = document.getElementById('auth-error-details');
|
||||
|
||||
const sendDeepLink = (type, value, key) => {
|
||||
try {
|
||||
const encodedValue = encodeURIComponent(value || '');
|
||||
const encodedServer = encodeURIComponent(serverUrl);
|
||||
const hashKey = key || 'access_token';
|
||||
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#${hashKey}=${encodedValue}&type=${type}`;
|
||||
window.location.href = deepLink;
|
||||
} catch (_) {
|
||||
// ignore deep link errors
|
||||
}
|
||||
};
|
||||
|
||||
const showError = (message, details) => {
|
||||
if (iconEl) {
|
||||
iconEl.textContent = '✗';
|
||||
iconEl.classList.add('error');
|
||||
}
|
||||
if (titleEl) {
|
||||
titleEl.textContent = 'Authentication failed';
|
||||
}
|
||||
if (messageEl) {
|
||||
messageEl.textContent = message;
|
||||
}
|
||||
if (detailsEl && details) {
|
||||
detailsEl.textContent = details;
|
||||
detailsEl.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
if (token) {
|
||||
// Extract nonce from URL to send back to desktop app for validation
|
||||
// (System browser doesn't have access to desktop app's sessionStorage)
|
||||
const nonceFromUrl = hashParams.get('nonce') || searchParams.get('nonce');
|
||||
|
||||
console.log('[Auth Callback] Token received, sending to desktop app via deep link');
|
||||
|
||||
// Send token + nonce via deep link to desktop app
|
||||
// Desktop app will validate nonce before accepting token
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const encodedToken = encodeURIComponent(token);
|
||||
const encodedServer = encodeURIComponent(serverUrl);
|
||||
const encodedNonce = nonceFromUrl ? encodeURIComponent(nonceFromUrl) : '';
|
||||
const deepLink = `stirlingpdf://auth/sso-complete?server=${encodedServer}#access_token=${encodedToken}&nonce=${encodedNonce}&type=sso-selfhosted`;
|
||||
window.location.href = deepLink;
|
||||
} catch (err) {
|
||||
console.error('[Auth Callback] Failed to trigger deep link:', err);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorCode) {
|
||||
const isCancelled = errorCode === 'access_denied';
|
||||
sendDeepLink('sso-error', errorCode, 'error');
|
||||
showError(
|
||||
isCancelled
|
||||
? 'Authentication was cancelled. You can close this window and return to the app.'
|
||||
: 'Authentication was not successful. You can close this window and return to the app.',
|
||||
errorCode
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showError(
|
||||
'Authentication did not complete. You can close this window and try again.',
|
||||
'missing_token'
|
||||
);
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', run);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon" id="auth-icon">✓</div>
|
||||
<h1 id="auth-title">Authentication complete</h1>
|
||||
<p id="auth-message">You can close this window and return to Stirling PDF.</p>
|
||||
<div class="error-details" id="auth-error-details"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
.formatted(escapedBaseUrlHtml, serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user