Various fixes and cleanup, regressions from prior commits.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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 ''
|
||||||
|
|||||||
@@ -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}` }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user