Improved auth profile UX, consistent transparent-blur dialog background everywhere.
This commit is contained in:
@@ -35,15 +35,35 @@ let authPromise = null
|
||||
let authResolve = null
|
||||
let authReject = null
|
||||
|
||||
/**
|
||||
* Check if an auth iframe is already open (from any source).
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAuthIframeOpen() {
|
||||
return !!document.getElementById('auth-iframe')
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns {Promise<void>}
|
||||
* @throws {AuthCancelledError} - If authentication is cancelled by user
|
||||
*/
|
||||
function showAuthIframe(iframeSrc) {
|
||||
// If already showing auth, return existing promise
|
||||
export function showAuthIframe(iframeSrc) {
|
||||
// If we already have a promise (from us), return it
|
||||
if (authPromise) return authPromise
|
||||
|
||||
// If there's already an iframe in the DOM (from App.vue or elsewhere),
|
||||
// create a promise that hooks into the message handler
|
||||
if (document.getElementById('auth-iframe')) {
|
||||
authPromise = new Promise((resolve, reject) => {
|
||||
authResolve = resolve
|
||||
authReject = reject
|
||||
})
|
||||
return authPromise
|
||||
}
|
||||
|
||||
authPromise = new Promise((resolve, reject) => {
|
||||
authResolve = resolve
|
||||
authReject = reject
|
||||
@@ -144,6 +164,11 @@ export async function apiFetch(url, options = {}) {
|
||||
}
|
||||
|
||||
if (authInfo?.iframe) {
|
||||
// If an auth iframe is already open (from app or another request), don't open another
|
||||
// Just return the response so the caller can handle it
|
||||
if (isAuthIframeOpen()) {
|
||||
return response
|
||||
}
|
||||
// Show auth iframe and wait for success (throws AuthCancelledError on cancel)
|
||||
await showAuthIframe(authInfo.iframe)
|
||||
// Loop to retry the original request
|
||||
|
||||
@@ -58,10 +58,14 @@ class AwaitableWebSocket extends WebSocket {
|
||||
console.error("Failed to parse JSON from WebSocket message", data, err)
|
||||
throw new Error("Failed to parse JSON from WebSocket message")
|
||||
}
|
||||
if (parsed.detail) {
|
||||
throw new Error(parsed.detail)
|
||||
// Wrap in response-like object with ok based on status field
|
||||
// Status 2xx = ok, 4xx/5xx = not ok, no status = ok (normal response)
|
||||
const status = parsed.status || 200
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
data: parsed,
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
send_json(data) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
|
||||
import aWebSocket from '@/utils/awaitable-websocket'
|
||||
import { getSettings } from '@/utils/settings'
|
||||
import { showAuthIframe } from '@/utils/api'
|
||||
|
||||
// Generic path normalizer: if an auth_host is configured and differs from current
|
||||
// host, return absolute URL (scheme derived by aWebSocket). Otherwise, keep as-is.
|
||||
@@ -10,34 +11,65 @@ async function makeUrl(path) {
|
||||
return h && location.host !== h ? `//${h}${path}` : path
|
||||
}
|
||||
|
||||
export async function register(resetToken = null, displayName = null) {
|
||||
export async function register(resetToken = null, displayName = null, onstartreg = null) {
|
||||
let params = []
|
||||
if (resetToken) params.push(`reset=${encodeURIComponent(resetToken)}`)
|
||||
if (displayName) params.push(`name=${encodeURIComponent(displayName)}`)
|
||||
const qs = params.length ? `?${params.join('&')}` : ''
|
||||
const ws = await aWebSocket(await makeUrl(`/auth/ws/register${qs}`))
|
||||
try {
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const registrationResponse = await startRegistration({ optionsJSON })
|
||||
ws.send_json(registrationResponse)
|
||||
return await ws.receive_json()
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error)
|
||||
// Replace useless and ugly error message from startRegistration
|
||||
throw Error(error.name === "NotAllowedError" ? 'Passkey registration cancelled' : error.message)
|
||||
} finally {
|
||||
ws.close()
|
||||
|
||||
while (true) {
|
||||
const ws = await aWebSocket(await makeUrl(`/auth/ws/register${qs}`))
|
||||
try {
|
||||
const res = await ws.receive_json()
|
||||
|
||||
// Handle auth errors (401/403) with iframe
|
||||
if ((res.status === 401 || res.status === 403) && res.data.auth?.iframe) {
|
||||
ws.close()
|
||||
await showAuthIframe(res.data.auth.iframe)
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
if (!res.ok) {
|
||||
throw new Error(res.data.detail || `Registration failed: ${res.status}`)
|
||||
}
|
||||
|
||||
// Notify caller that we're about to show the browser prompt
|
||||
if (onstartreg) onstartreg()
|
||||
|
||||
const registrationResponse = await startRegistration({ optionsJSON: res.data })
|
||||
ws.send_json(registrationResponse)
|
||||
|
||||
const result = await ws.receive_json()
|
||||
if (!result.ok) {
|
||||
throw new Error(result.data.detail || `Registration failed: ${result.status}`)
|
||||
}
|
||||
return result.data
|
||||
} catch (error) {
|
||||
ws.close()
|
||||
console.error('Registration error:', error)
|
||||
// Replace useless and ugly error message from startRegistration
|
||||
throw Error(error.name === "NotAllowedError" ? 'Passkey registration cancelled' : error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticate() {
|
||||
const ws = await aWebSocket(await makeUrl('/auth/ws/authenticate'))
|
||||
try {
|
||||
const optionsJSON = await ws.receive_json()
|
||||
const authResponse = await startAuthentication({ optionsJSON })
|
||||
const res = await ws.receive_json()
|
||||
if (!res.ok) {
|
||||
throw new Error(res.data.detail || `Authentication failed: ${res.status}`)
|
||||
}
|
||||
|
||||
const authResponse = await startAuthentication({ optionsJSON: res.data })
|
||||
ws.send_json(authResponse)
|
||||
|
||||
const result = await ws.receive_json()
|
||||
return result
|
||||
if (!result.ok) {
|
||||
throw new Error(result.data.detail || `Authentication failed: ${result.status}`)
|
||||
}
|
||||
return result.data
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error)
|
||||
throw Error(error.name === "NotAllowedError" ? 'Passkey authentication cancelled' : error.message)
|
||||
|
||||
Reference in New Issue
Block a user