Update forward API to return in JSON iframe srcdoc with options injected. (currently broken in dev mode).

This commit is contained in:
2025-12-04 01:58:18 +00:00
parent 4482a601f3
commit b9b1c995f9
10 changed files with 111 additions and 63 deletions
+31 -6
View File
@@ -103,7 +103,32 @@
}
});
function showLoginMode() {
// Cache for auth iframe HTML by mode
const authIframeHtmlCache = {};
async function getAuthIframeHtml(mode = 'login') {
if (authIframeHtmlCache[mode]) {
return authIframeHtmlCache[mode];
}
// Fetch from forward endpoint - it returns HTML in auth.iframe on 401/403
const response = await fetch('/auth/api/forward', { credentials: 'include' });
if (response.status === 401 || response.status === 403) {
const data = await response.json();
if (data.auth?.iframe) {
let html = data.auth.iframe;
if (mode !== data.auth.mode) {
// Replace data-mode attribute value
html = html.replace(/data-mode="[^"]*"/, `data-mode="${mode}"`);
}
authIframeHtmlCache[mode] = html;
return html;
}
}
throw new Error('Unable to fetch auth iframe HTML');
}
async function showLoginMode() {
let iframe = document.getElementById('auth-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
@@ -111,11 +136,11 @@
iframe.title = 'Authentication';
document.body.appendChild(iframe);
}
iframe.src = '/auth/restricted/?mode=login';
iframe.srcdoc = await getAuthIframeHtml('login');
showStatus('Login mode loaded - for users who are not authenticated', 'info');
}
function showReauthMode() {
async function showReauthMode() {
let iframe = document.getElementById('auth-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
@@ -123,17 +148,17 @@
iframe.title = 'Authentication';
document.body.appendChild(iframe);
}
iframe.src = '/auth/restricted/?mode=reauth';
iframe.srcdoc = await getAuthIframeHtml('reauth');
showStatus('Reauth mode loaded - for additional verification of authenticated users', 'info');
}
function showAuthIframe() {
async function showAuthIframe() {
let iframe = document.getElementById('auth-iframe');
if (!iframe) {
iframe = document.createElement('iframe');
iframe.id = 'auth-iframe';
iframe.title = 'Authentication';
iframe.src = '/auth/restricted/';
iframe.srcdoc = await getAuthIframeHtml('login');
document.body.appendChild(iframe);
iframeInitialized = true;
}
+5 -4
View File
@@ -12,7 +12,7 @@
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { apiJson } from '@/utils/api'
import { apiJson, getAuthIframeHtml } from '@/utils/api'
import StatusMessage from '@/components/StatusMessage.vue'
import ProfileView from '@/components/ProfileView.vue'
import LoadingView from '@/components/LoadingView.vue'
@@ -39,15 +39,16 @@ async function tryLoadUserInfo() {
}
}
function showAuthIframe() {
async function showAuthIframe() {
// Remove existing iframe if any
hideAuthIframe()
// Create new iframe for authentication
// Create new iframe for authentication using srcdoc
const html = await getAuthIframeHtml('login')
authIframe = document.createElement('iframe')
authIframe.id = 'auth-iframe'
authIframe.title = 'Authentication'
authIframe.src = '/auth/restricted/?mode=login'
authIframe.srcdoc = html
document.body.appendChild(authIframe)
loadingMessage.value = 'Authentication required...'
}
+4 -3
View File
@@ -13,7 +13,7 @@ import AdminUserDetail from '@/admin/AdminUserDetail.vue'
import AdminDialogs from '@/admin/AdminDialogs.vue'
import { useAuthStore } from '@/stores/auth'
import { getSettings, adminUiPath, makeUiHref } from '@/utils/settings'
import { apiJson } from '@/utils/api'
import { apiJson, getAuthIframeHtml } from '@/utils/api'
const info = ref(null)
const loading = ref(true)
@@ -288,12 +288,13 @@ function deletePermission(p) {
} })
}
function showAuthIframe() {
async function showAuthIframe() {
hideAuthIframe()
const html = await getAuthIframeHtml('login')
authIframe = document.createElement('iframe')
authIframe.id = 'auth-iframe'
authIframe.title = 'Authentication'
authIframe.src = '/auth/restricted/?mode=login'
authIframe.srcdoc = html
document.body.appendChild(authIframe)
loadingMessage.value = 'Authentication required...'
}
+6 -2
View File
@@ -10,9 +10,13 @@
import { computed, onMounted } from 'vue'
import RestrictedAuth from '@/components/RestrictedAuth.vue'
// Detect mode from data attribute on html tag (injected by server)
const authMode = computed(() => {
const params = new URLSearchParams(window.location.search)
return params.get('mode') === 'reauth' ? 'reauth' : 'login'
const htmlElement = document.documentElement
const dataMode = htmlElement.getAttribute('data-mode')
if (dataMode === 'reauth') return 'reauth'
if (dataMode === 'forbidden') return 'forbidden'
return 'login'
})
function postToParent(message) {
+38 -4
View File
@@ -49,6 +49,40 @@ let authPromise = null
let authResolve = null
let authReject = null
// Cache for auth iframe HTML by mode
const authIframeHtmlCache = {}
/**
* Get the auth iframe HTML for a given mode.
* Fetches from /auth/api/forward which returns HTML in the auth.iframe field.
* Results are cached per mode.
* @param {string} mode - The auth mode ('login', 'reauth', 'forbidden')
* @returns {Promise<string>} - The HTML content for the iframe
*/
export async function getAuthIframeHtml(mode = 'login') {
if (authIframeHtmlCache[mode]) {
return authIframeHtmlCache[mode]
}
// Fetch from forward endpoint - it returns HTML in auth.iframe on 401/403
const response = await fetch('/auth/api/forward', { credentials: 'include' })
if (response.status === 401 || response.status === 403) {
const data = await response.json()
if (data.auth?.iframe) {
// Cache the HTML - it's the same regardless of mode (mode is in data attrs)
// But we need to patch the mode in the HTML if different from returned
let html = data.auth.iframe
if (mode !== data.auth.mode) {
// Replace data-mode attribute value
html = html.replace(/data-mode="[^"]*"/, `data-mode="${mode}"`)
}
authIframeHtmlCache[mode] = html
return html
}
}
throw new Error('Unable to fetch auth iframe HTML')
}
/**
* Check if an auth iframe is already open (from any source).
* @returns {boolean}
@@ -60,11 +94,11 @@ export function isAuthIframeOpen() {
/**
* Show the authentication iframe and return a promise that resolves on success.
* If an auth iframe is already open (from any source), hooks into its completion.
* @param {string} iframeSrc - The URL for the iframe src
* @param {string} iframeHtml - The HTML content for the iframe srcdoc
* @returns {Promise<void>}
* @throws {AuthCancelledError} - If authentication is cancelled by user
*/
export function showAuthIframe(iframeSrc) {
export function showAuthIframe(iframeHtml) {
// If we already have a promise (from us), return it
if (authPromise) return authPromise
@@ -86,11 +120,11 @@ export function showAuthIframe(iframeSrc) {
// Remove existing iframe if any
hideAuthIframe()
// Create new iframe for authentication
// Create new iframe for authentication using srcdoc
authIframe = document.createElement('iframe')
authIframe.id = 'auth-iframe'
authIframe.title = 'Authentication'
authIframe.src = iframeSrc
authIframe.srcdoc = iframeHtml
document.body.appendChild(authIframe)
return authPromise
+1 -8
View File
@@ -33,14 +33,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
"""Handle AuthException with auth info for UI."""
return JSONResponse(
status_code=exc.status_code,
content={
"detail": exc.detail,
"auth": {
"mode": exc.mode,
"iframe": f"/auth/restricted/?mode={exc.mode}",
**exc.metadata,
},
},
content=authz.auth_error_content(exc),
)
+5 -21
View File
@@ -62,14 +62,7 @@ async def auth_exception_handler(_request: Request, exc: authz.AuthException):
"""Handle AuthException with auth info for UI."""
return JSONResponse(
status_code=exc.status_code,
content={
"detail": exc.detail,
"auth": {
"mode": exc.mode,
"iframe": f"/auth/restricted/?mode={exc.mode}",
**exc.metadata,
},
},
content=authz.auth_error_content(exc),
)
@@ -184,27 +177,18 @@ async def forward_authentication(
wants_html = "text/html" in accept
if wants_html:
# Browser request - return HTML with metadata
html = frontend.file("int", "forward", "index.html").read_bytes()
# Inject mode and any additional metadata
# Browser request - return full-page HTML with metadata
data_attrs = {"mode": e.mode, **e.metadata}
html = frontend.file("int", "forward", "index.html").read_bytes()
html = htmlutil.patch_html_data_attrs(html, **data_attrs)
return Response(
html, status_code=e.status_code, media_type="text/html; charset=UTF-8"
)
else:
# API request - return JSON with iframe src link
iframe_url = f"/auth/restricted/?mode={e.mode}"
# API request - return JSON with iframe srcdoc HTML
return JSONResponse(
status_code=e.status_code,
content={
"detail": e.detail,
"auth": {
"mode": e.mode,
"iframe": iframe_url,
**e.metadata,
},
},
content=authz.auth_error_content(e),
)
+19 -1
View File
@@ -2,7 +2,7 @@ import logging
from fastapi import HTTPException
from ..util import permutil, sessionutil
from ..util import frontend, htmlutil, permutil, sessionutil
logger = logging.getLogger(__name__)
@@ -32,6 +32,24 @@ class AuthException(HTTPException):
self.metadata = metadata
def auth_error_content(exc: AuthException) -> dict:
"""Generate JSON response content for an AuthException.
Returns a dict with detail, mode, and iframe HTML for srcdoc embedding.
"""
data_attrs = {"mode": exc.mode, **exc.metadata}
iframe_html = frontend.file("auth", "restricted", "index.html").read_bytes()
iframe_html = htmlutil.patch_html_data_attrs(iframe_html, **data_attrs)
return {
"detail": exc.detail,
"auth": {
"mode": exc.mode,
"iframe": iframe_html.decode("utf-8"),
**exc.metadata,
},
}
async def verify(
auth: str | None,
perm: list[str],
+1 -8
View File
@@ -29,14 +29,7 @@ async def auth_exception_handler(_request, exc: authz.AuthException):
"""Handle AuthException with auth info for UI."""
return JSONResponse(
status_code=exc.status_code,
content={
"detail": exc.detail,
"auth": {
"mode": exc.mode,
"iframe": f"/auth/restricted/?mode={exc.mode}",
**exc.metadata,
},
},
content=authz.auth_error_content(exc),
)
+1 -6
View File
@@ -26,12 +26,7 @@ def websocket_error_handler(func):
await ws.send_json(
{
"status": e.status_code,
"detail": e.detail,
"auth": {
"mode": e.mode,
"iframe": f"/auth/restricted/?mode={e.mode}",
**e.metadata,
},
**authz.auth_error_content(e),
}
)
except (ValueError, InvalidAuthenticationResponse) as e: