paskia-js: profile() dialog, auth-logout message, host color-scheme adoption
New profile() function opens the minimal profile in a compact dialog
iframe and always resolves ('logout' | 'back'), keeping the auth flow's
resolve/reject contract separate and unchanged. The overlay now injects
the host page's computed color-scheme into the iframe URL theme param
when the server has not provided one.
This commit is contained in:
@@ -20,6 +20,7 @@ export {
|
|||||||
isAuthIframeOpen,
|
isAuthIframeOpen,
|
||||||
hideAuthIframe,
|
hideAuthIframe,
|
||||||
showAuthIframe,
|
showAuthIframe,
|
||||||
|
profile,
|
||||||
} from './overlay'
|
} from './overlay'
|
||||||
|
|
||||||
export { SessionValidator } from './validate'
|
export { SessionValidator } from './validate'
|
||||||
|
|||||||
@@ -32,12 +32,24 @@ body.paskia-backdrop {
|
|||||||
color-scheme: auto;
|
color-scheme: auto;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
#${AUTH_IFRAME_ID}.paskia-dialog {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: min(36rem, 100%);
|
||||||
|
height: min(42rem, 100%);
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
type DialogResult = 'logout' | 'back'
|
||||||
|
|
||||||
let authIframe: HTMLIFrameElement | null = null
|
let authIframe: HTMLIFrameElement | null = null
|
||||||
let authPromise: Promise<void> | null = null
|
let authPromise: Promise<DialogResult | undefined> | null = null
|
||||||
let authResolve: (() => void) | null = null
|
let authResolve: ((result?: DialogResult) => void) | null = null
|
||||||
let authReject: ((error: Error) => void) | null = null
|
let authReject: ((error: Error) => void) | null = null
|
||||||
|
// Auth flows reject AuthCancelledError on auth-back (callers rely on it to
|
||||||
|
// abort request retries); the profile dialog resolves 'back' instead.
|
||||||
|
let cancelAsError = true
|
||||||
let messageListenerInstalled = false
|
let messageListenerInstalled = false
|
||||||
let backdropHolders = 0
|
let backdropHolders = 0
|
||||||
|
|
||||||
@@ -98,8 +110,20 @@ function handleAuthMessage(event: MessageEvent): void {
|
|||||||
|
|
||||||
case 'auth-back':
|
case 'auth-back':
|
||||||
hideAuthIframe()
|
hideAuthIframe()
|
||||||
if (authReject) {
|
if (cancelAsError && authReject) {
|
||||||
authReject(new AuthCancelledError())
|
authReject(new AuthCancelledError())
|
||||||
|
} else if (authResolve) {
|
||||||
|
authResolve('back')
|
||||||
|
}
|
||||||
|
authPromise = null
|
||||||
|
authResolve = null
|
||||||
|
authReject = null
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'auth-logout':
|
||||||
|
hideAuthIframe()
|
||||||
|
if (authResolve) {
|
||||||
|
authResolve('logout')
|
||||||
authPromise = null
|
authPromise = null
|
||||||
authResolve = null
|
authResolve = null
|
||||||
authReject = null
|
authReject = null
|
||||||
@@ -116,12 +140,15 @@ function ensureMessageListener(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Promise<void> {
|
function openIframe(iframeUrl: string, title: string, dialog: boolean, cancelError: boolean): Promise<DialogResult | undefined> {
|
||||||
injectStyles()
|
injectStyles()
|
||||||
ensureMessageListener()
|
ensureMessageListener()
|
||||||
|
|
||||||
if (authPromise) return authPromise
|
if (authPromise) return authPromise
|
||||||
|
|
||||||
|
cancelAsError = cancelError
|
||||||
|
iframeUrl = withAppTheme(iframeUrl)
|
||||||
|
|
||||||
if (document.getElementById(AUTH_IFRAME_ID)) {
|
if (document.getElementById(AUTH_IFRAME_ID)) {
|
||||||
authPromise = new Promise((resolve, reject) => {
|
authPromise = new Promise((resolve, reject) => {
|
||||||
authResolve = resolve
|
authResolve = resolve
|
||||||
@@ -140,6 +167,7 @@ export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Pro
|
|||||||
|
|
||||||
authIframe = document.createElement('iframe')
|
authIframe = document.createElement('iframe')
|
||||||
authIframe.id = AUTH_IFRAME_ID
|
authIframe.id = AUTH_IFRAME_ID
|
||||||
|
if (dialog) authIframe.classList.add('paskia-dialog')
|
||||||
authIframe.title = title
|
authIframe.title = title
|
||||||
authIframe.src = iframeUrl
|
authIframe.src = iframeUrl
|
||||||
document.body.appendChild(authIframe)
|
document.body.appendChild(authIframe)
|
||||||
@@ -147,6 +175,48 @@ export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Pro
|
|||||||
return authPromise
|
return authPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect the host page's own color scheme (CSS color-scheme on body) as an
|
||||||
|
// implicit app-level default. Only an unambiguous 'light' or 'dark' counts;
|
||||||
|
// 'normal', 'light dark' etc. mean the page adapts, so no override is needed.
|
||||||
|
function detectColorScheme(): string {
|
||||||
|
if (typeof window === 'undefined' || !document.body) return ''
|
||||||
|
const scheme = getComputedStyle(document.body).colorScheme
|
||||||
|
return scheme === 'light' || scheme === 'dark' ? scheme : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the host page's own color scheme to the iframe URL hash — only when
|
||||||
|
// the URL has no theme parameter yet (a server-provided user theme override
|
||||||
|
// is authoritative). The restricted UI's precedence is: URL parameter (user
|
||||||
|
// override from the server, else host color scheme) > cached profile theme
|
||||||
|
// (localStorage) > browser/desktop default.
|
||||||
|
function withAppTheme(iframeUrl: string): string {
|
||||||
|
const theme = detectColorScheme()
|
||||||
|
if (!theme) return iframeUrl
|
||||||
|
const hashIndex = iframeUrl.indexOf('#')
|
||||||
|
const base = hashIndex === -1 ? iframeUrl : iframeUrl.slice(0, hashIndex)
|
||||||
|
const params = new URLSearchParams(hashIndex === -1 ? '' : iframeUrl.slice(hashIndex + 1))
|
||||||
|
if (params.has('theme')) return iframeUrl
|
||||||
|
params.set('theme', theme)
|
||||||
|
return `${base}#${params}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Promise<void> {
|
||||||
|
return openIframe(iframeUrl, title, false, true).then(() => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the minimal profile of the logged-in user in a compact dialog iframe.
|
||||||
|
*
|
||||||
|
* Unlike the auth flows, this always resolves — 'logout' when the user
|
||||||
|
* signed out inside the frame, 'back' when they closed it without action.
|
||||||
|
* The caller decides from context how to react to each (e.g. whether to
|
||||||
|
* start a new login attempt with showAuthIframe).
|
||||||
|
*/
|
||||||
|
export function profile(): Promise<DialogResult> {
|
||||||
|
return openIframe('/auth/restricted/iframe#mode=profile', 'Profile', true, false)
|
||||||
|
.then((result) => result ?? 'back')
|
||||||
|
}
|
||||||
|
|
||||||
export function createAuthIframe(iframeUrl: string, title = 'Authentication'): HTMLIFrameElement {
|
export function createAuthIframe(iframeUrl: string, title = 'Authentication'): HTMLIFrameElement {
|
||||||
injectStyles()
|
injectStyles()
|
||||||
const existing = document.getElementById(AUTH_IFRAME_ID)
|
const existing = document.getElementById(AUTH_IFRAME_ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user