Various fixes and cleanup, regressions from prior commits.

This commit is contained in:
2025-12-04 03:40:59 +00:00
parent 6124fa6c01
commit 9976e05696
10 changed files with 87 additions and 67 deletions
+5 -6
View File
@@ -26,15 +26,14 @@ const showBackMessage = ref(false)
let validationTimer = null let validationTimer = null
let authIframe = null let authIframe = null
async function tryLoadUserInfo() { async function loadUserInfo() {
try { try {
await store.loadUserInfo() store.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
authenticated.value = true authenticated.value = true
loading.value = false loading.value = false
startSessionValidation() startSessionValidation()
return true return true
} catch (error) { } catch (e) {
// User info load failed - apiJson will show iframe if needed
return false return false
} }
} }
@@ -74,7 +73,7 @@ function handleAuthMessage(event) {
hideAuthIframe() hideAuthIframe()
loading.value = true loading.value = true
loadingMessage.value = 'Loading user profile...' loadingMessage.value = 'Loading user profile...'
tryLoadUserInfo() loadUserInfo()
break break
case 'auth-error': case 'auth-error':
@@ -150,7 +149,7 @@ onMounted(async () => {
if (store.settings?.rp_name) document.title = store.settings.rp_name if (store.settings?.rp_name) document.title = store.settings.rp_name
// Try to load user info // Try to load user info
const success = await tryLoadUserInfo() const success = await loadUserInfo()
if (!success) { if (!success) {
// Need authentication - show login iframe // Need authentication - show login iframe
+15 -6
View File
@@ -152,27 +152,36 @@ async function loadPermissions() {
permissions.value = await apiJson('/auth/api/admin/permissions') permissions.value = await apiJson('/auth/api/admin/permissions')
} }
async function loadUserInfo() {
try {
info.value = await apiJson('/auth/api/user-info', { method: 'POST' })
authenticated.value = true
return true
} catch (e) {
error.value = e.message
return false
}
}
async function load() { async function load() {
loading.value = true loading.value = true
loadingMessage.value = 'Loading...' loadingMessage.value = 'Loading...'
error.value = null error.value = null
try { try {
const data = await apiJson('/auth/api/user-info', { method: 'POST' }) if (!await loadUserInfo()) return
info.value = data
authenticated.value = true
// Check if user has required permissions // Check if user has required permissions
if (data.authenticated && !(data.is_global_admin || data.is_org_admin)) { if (info.value.authenticated && !(info.value.is_global_admin || info.value.is_org_admin)) {
// User is authenticated but lacks required permissions - show forbidden view // User is authenticated but lacks required permissions - show forbidden view
error.value = 'You do not have permission to access this area.' error.value = 'You do not have permission to access this area.'
loading.value = false loading.value = false
return return
} }
if (data.authenticated && (data.is_global_admin || data.is_org_admin)) { if (info.value.authenticated && (info.value.is_global_admin || info.value.is_org_admin)) {
await Promise.all([loadOrgs(), loadPermissions()]) await Promise.all([loadOrgs(), loadPermissions()])
} }
if (!data.is_global_admin && data.is_org_admin && orgs.value.length === 1) { if (!info.value.is_global_admin && info.value.is_org_admin && orgs.value.length === 1) {
if (!window.location.hash || window.location.hash === '#overview') { if (!window.location.hash || window.location.hash === '#overview') {
currentOrgId.value = orgs.value[0].uuid currentOrgId.value = orgs.value[0].uuid
window.location.hash = `#org/${currentOrgId.value}` window.location.hash = `#org/${currentOrgId.value}`
+9 -13
View File
@@ -145,7 +145,7 @@ async function registerPasskey() {
} }
try { try {
await setSessionCookie(result.session_token) await setSessionCookie(result)
} catch (error) { } catch (error) {
loading.value = false loading.value = false
const message = error?.message || 'Failed to establish session' const message = error?.message || 'Failed to establish session'
@@ -153,23 +153,23 @@ async function registerPasskey() {
return return
} }
showMessage('Passkey registered successfully!', 'success', 2000) showMessage('Passkey registered successfully!', 'success', 800)
setTimeout(() => { setTimeout(() => { loading.value = false; goHome() }, 800)
loading.value = false
redirectHome()
}, 800)
} }
async function setSessionCookie(sessionToken) { async function setSessionCookie(result) {
if (!result?.session_token) {
throw new Error('Registration response missing session_token')
}
return await apiJson('/auth/api/set-session', { return await apiJson('/auth/api/set-session', {
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${sessionToken}` Authorization: `Bearer ${result.session_token}`
} }
}) })
} }
function redirectHome() { function goHome() {
const target = uiBasePath.value || '/auth/' const target = uiBasePath.value || '/auth/'
if (window.location.pathname !== target) { if (window.location.pathname !== target) {
history.replaceState(null, '', target) history.replaceState(null, '', target)
@@ -177,10 +177,6 @@ function redirectHome() {
window.location.reload() window.location.reload()
} }
function goHome() {
redirectHome()
}
function extractTokenFromPath() { function extractTokenFromPath() {
const segments = window.location.pathname.split('/').filter(Boolean) const segments = window.location.pathname.split('/').filter(Boolean)
if (!segments.length) return '' if (!segments.length) return ''
+7 -3
View File
@@ -153,7 +153,7 @@ async function authenticateUser() {
emit('auth-error', { message, cancelled }) emit('auth-error', { message, cancelled })
return return
} }
try { await setSessionCookie(result.session_token) } catch (error) { try { await setSessionCookie(result) } catch (error) {
loading.value = false loading.value = false
const message = error?.message || 'Failed to establish session' const message = error?.message || 'Failed to establish session'
showMessage(message, 'error', 4000) showMessage(message, 'error', 4000)
@@ -186,9 +186,13 @@ function openProfile() {
if (profileWindow) profileWindow.focus() if (profileWindow) profileWindow.focus()
} }
async function setSessionCookie(sessionToken) { async function setSessionCookie(result) {
if (!result?.session_token) {
console.error('setSessionCookie called with missing session_token:', result)
throw new Error('Authentication response missing session_token')
}
return await apiJson('/auth/api/set-session', { return await apiJson('/auth/api/set-session', {
method: 'POST', headers: { Authorization: `Bearer ${sessionToken}` } method: 'POST', headers: { Authorization: `Bearer ${result.session_token}` }
}) })
} }
+23 -9
View File
@@ -38,18 +38,21 @@ export const useAuthStore = defineStore('auth', {
}, duration) }, duration)
} }
}, },
async setSessionCookie(sessionToken) { async setSessionCookie(result) {
const result = await apiJson('/auth/api/set-session', { if (!result?.session_token) {
console.error('setSessionCookie called with missing session_token:', result)
throw new Error('Authentication response missing session_token')
}
return await apiJson('/auth/api/set-session', {
method: 'POST', method: 'POST',
headers: {'Authorization': `Bearer ${sessionToken}`}, headers: {'Authorization': `Bearer ${result.session_token}`},
}) })
return result
}, },
async register() { async register() {
this.isLoading = true this.isLoading = true
try { try {
const result = await register() const result = await register()
await this.setSessionCookie(result.session_token) await this.setSessionCookie(result)
await this.loadUserInfo() await this.loadUserInfo()
this.selectView() this.selectView()
return result return result
@@ -62,7 +65,7 @@ export const useAuthStore = defineStore('auth', {
try { try {
const result = await authenticate() const result = await authenticate()
await this.setSessionCookie(result.session_token) await this.setSessionCookie(result)
await this.loadUserInfo() await this.loadUserInfo()
this.selectView() this.selectView()
@@ -83,7 +86,12 @@ export const useAuthStore = defineStore('auth', {
this.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' }) this.userInfo = await apiJson('/auth/api/user-info', { method: 'POST' })
console.log('User info loaded:', this.userInfo) console.log('User info loaded:', this.userInfo)
} catch (error) { } catch (error) {
this.showMessage(error.message || 'Failed to load user info', 'error', 5000) // Suppress toast for 401/403 errors - the auth iframe will handle these
if (error.status === 401 || error.status === 403) {
console.log('Authentication required:', error.message)
} else {
this.showMessage(error.message || 'Failed to load user info', 'error', 5000)
}
throw error throw error
} }
}, },
@@ -113,7 +121,10 @@ export const useAuthStore = defineStore('auth', {
location.reload() location.reload()
} catch (error) { } catch (error) {
console.error('Logout error:', error) console.error('Logout error:', error)
this.showMessage(error.message, 'error') // Suppress toast for 401/403 errors - the auth iframe will handle these
if (error.status !== 401 && error.status !== 403) {
this.showMessage(error.message, 'error')
}
} }
}, },
async logoutEverywhere() { async logoutEverywhere() {
@@ -123,7 +134,10 @@ export const useAuthStore = defineStore('auth', {
location.reload() location.reload()
} catch (error) { } catch (error) {
console.error('Logout-all error:', error) console.error('Logout-all error:', error)
this.showMessage(error.message, 'error') // Suppress toast for 401/403 errors - the auth iframe will handle these
if (error.status !== 401 && error.status !== 403) {
this.showMessage(error.message, 'error')
}
} }
}, },
} }
+2
View File
@@ -315,6 +315,8 @@ export function shouldShowErrorToast(error) {
// Don't show toast for user cancellations // Don't show toast for user cancellations
if (error instanceof AuthCancelledError) return false if (error instanceof AuthCancelledError) return false
if (error.name === 'AbortError') return false if (error.name === 'AbortError') return false
// Don't show toast for 401/403 errors - the auth iframe will handle these
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) return false
return true return true
} }
+3 -12
View File
@@ -5,8 +5,8 @@ class AwaitableWebSocket extends WebSocket {
#opened = false #opened = false
constructor(resolve, reject, url, protocols, binaryType) { constructor(resolve, reject, url, protocols, binaryType) {
// Support relative URLs even on old browsers that don't // Support relative URLs even on old browsers that don't natively support them
super(new URL(url, location.href.replace(/^http/, 'ws')), protocols) super(new URL(url, document.baseURI.replace(/^http/, 'ws')), protocols)
this.binaryType = binaryType || 'blob' this.binaryType = binaryType || 'blob'
this.onopen = () => { this.onopen = () => {
this.#opened = true this.#opened = true
@@ -51,21 +51,12 @@ class AwaitableWebSocket extends WebSocket {
console.error("WebSocket received binary data, expected JSON string", data) console.error("WebSocket received binary data, expected JSON string", data)
throw new Error("WebSocket received binary data, expected JSON string") throw new Error("WebSocket received binary data, expected JSON string")
} }
let parsed
try { try {
parsed = JSON.parse(data) return JSON.parse(data)
} catch (err) { } catch (err) {
console.error("Failed to parse JSON from WebSocket message", data, err) console.error("Failed to parse JSON from WebSocket message", data, err)
throw new Error("Failed to parse JSON from WebSocket message") throw new Error("Failed to parse JSON from WebSocket message")
} }
// 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,
}
} }
send_json(data) { send_json(data) {
+16 -15
View File
@@ -23,28 +23,28 @@ export async function register(resetToken = null, displayName = null, onstartreg
const res = await ws.receive_json() const res = await ws.receive_json()
// Handle auth errors (401/403) with iframe // Handle auth errors (401/403) with iframe
if ((res.status === 401 || res.status === 403) && res.data.auth?.iframe) { if ((res.status === 401 || res.status === 403) && res.auth?.iframe) {
ws.close() ws.close()
await showAuthIframe(res.data.auth.iframe) await showAuthIframe(res.auth.iframe)
continue continue
} }
// Handle other errors // Handle other errors (status field present means error)
if (!res.ok) { if (res.status) {
throw new Error(res.data.detail || `Registration failed: ${res.status}`) throw new Error(res.detail || `Registration failed: ${res.status}`)
} }
// Notify caller that we're about to show the browser prompt // Notify caller that we're about to show the browser prompt
if (onstartreg) onstartreg() if (onstartreg) onstartreg()
const registrationResponse = await startRegistration({ optionsJSON: res.data }) const registrationResponse = await startRegistration({ optionsJSON: res })
ws.send_json(registrationResponse) ws.send_json(registrationResponse)
const result = await ws.receive_json() const result = await ws.receive_json()
if (!result.ok) { if (result.status) {
throw new Error(result.data.detail || `Registration failed: ${result.status}`) throw new Error(result.detail || `Registration failed: ${result.status}`)
} }
return result.data return result
} catch (error) { } catch (error) {
ws.close() ws.close()
console.error('Registration error:', error) console.error('Registration error:', error)
@@ -58,18 +58,19 @@ export async function authenticate() {
const ws = await aWebSocket(await makeUrl('/auth/ws/authenticate')) const ws = await aWebSocket(await makeUrl('/auth/ws/authenticate'))
try { try {
const res = await ws.receive_json() const res = await ws.receive_json()
if (!res.ok) { // status field present means error
throw new Error(res.data.detail || `Authentication failed: ${res.status}`) if (res.status) {
throw new Error(res.detail || `Authentication failed: ${res.status}`)
} }
const authResponse = await startAuthentication({ optionsJSON: res.data }) const authResponse = await startAuthentication({ optionsJSON: res })
ws.send_json(authResponse) ws.send_json(authResponse)
const result = await ws.receive_json() const result = await ws.receive_json()
if (!result.ok) { if (result.status) {
throw new Error(result.data.detail || `Authentication failed: ${result.status}`) throw new Error(result.detail || `Authentication failed: ${result.status}`)
} }
return result.data return result
} catch (error) { } catch (error) {
console.error('Authentication error:', error) console.error('Authentication error:', error)
throw Error(error.name === "NotAllowedError" ? 'Passkey authentication cancelled' : error.message) throw Error(error.name === "NotAllowedError" ? 'Passkey authentication cancelled' : error.message)
+5 -1
View File
@@ -228,7 +228,11 @@ async def api_user_info(
target_user_uuid = reset_token.user_uuid target_user_uuid = reset_token.user_uuid
else: else:
if auth is None: if auth is None:
raise ValueError("Authentication Required") raise authz.AuthException(
status_code=401,
detail="Authentication required",
mode="login",
)
session_record = await get_session(auth, host=request.headers.get("host")) session_record = await get_session(auth, host=request.headers.get("host"))
authenticated = True authenticated = True
target_user_uuid = session_record.user_uuid target_user_uuid = session_record.user_uuid
+2 -2
View File
@@ -30,10 +30,10 @@ def websocket_error_handler(func):
} }
) )
except (ValueError, InvalidAuthenticationResponse) as e: except (ValueError, InvalidAuthenticationResponse) as e:
await ws.send_json({"detail": str(e)}) await ws.send_json({"status": 401, "detail": str(e)})
except Exception: except Exception:
logging.exception("Internal Server Error") logging.exception("Internal Server Error")
await ws.send_json({"detail": "Internal Server Error"}) await ws.send_json({"status": 500, "detail": "Internal Server Error"})
return wrapper return wrapper