Better error messages from backend, avoid bad toasts, cleanup of session validation.

This commit is contained in:
Leo Vasanko
2025-12-02 16:37:27 +00:00
parent 77d8e97dc9
commit a05d4aec81
8 changed files with 33 additions and 44 deletions
+3 -3
View File
@@ -574,10 +574,10 @@ async function submitDialog() {
<StatusMessage />
<main class="app-main">
<LoadingView v-if="loading" :message="loadingMessage" />
<AuthRequiredMessage
v-else-if="showBackMessage"
<AuthRequiredMessage
v-else-if="showBackMessage"
message="You need to authenticate to access the admin panel."
@reload="reloadPage"
@reload="reloadPage"
/>
<section v-else class="view-root view-root--wide view-admin">
<header class="view-header">
+1 -1
View File
@@ -135,7 +135,7 @@ async function authenticateUser() {
loading.value = false
const message = error?.message || 'Passkey authentication cancelled'
const cancelled = message === 'Passkey authentication cancelled'
showMessage(cancelled ? message : `Authentication failed: ${message}`, cancelled ? 'info' : 'error', 4000)
showMessage(message, cancelled ? 'info' : 'error', 4000)
emit('auth-error', { message, cancelled })
return
}
@@ -4,7 +4,6 @@
@authenticated="handleAuthenticated"
@forbidden="handleForbidden"
@logout="handleLogout"
@auth-error="handleAuthError"
@back="handleBack"
/>
</template>
@@ -51,15 +50,6 @@ function handleLogout() {
})
}
function handleAuthError({ message, cancelled }) {
// Notify parent that authentication failed or was cancelled
postToParent({
type: 'auth-error',
message: message || 'Authentication failed',
cancelled
})
}
function handleBack() {
console.log('[RestrictedApiApp] Back clicked')
// Notify parent that user wants to go back
+1 -1
View File
@@ -112,7 +112,7 @@ export const useAuthStore = defineStore('auth', {
throw new Error('Authentication required')
}
const result = await response.json()
if (result.detail) throw new Error(`Server: ${result.detail}`)
if (result.detail) throw new Error(result.detail)
await this.loadUserInfo()
},
+1 -1
View File
@@ -59,7 +59,7 @@ class AwaitableWebSocket extends WebSocket {
throw new Error("Failed to parse JSON from WebSocket message")
}
if (parsed.detail) {
throw new Error(`Server: ${parsed.detail}`)
throw new Error(parsed.detail)
}
return parsed
}
+14 -24
View File
@@ -69,36 +69,26 @@ async def create_session(
async def get_reset(token: str) -> ResetToken:
"""Validate a credential reset token. Returns None if the token is not well formed (i.e. it is another type of token)."""
record = await db.instance.get_reset_token(reset_key(token))
if not record:
raise ValueError("Invalid or expired session token")
if record.expiry < datetime.now(timezone.utc):
await db.instance.delete_reset_token(record.key)
raise ValueError("Invalid or expired session token")
return record
if record and record.expiry >= datetime.now(timezone.utc):
return record
raise ValueError("This reset link is invalid or has expired")
async def get_session(token: str, host: str | None = None) -> Session:
"""Validate a session token and return session data if valid."""
host = hostutil.normalize_host(host)
if not host:
raise ValueError("Invalid host")
session = await db.instance.get_session(session_key(token))
if not session:
raise ValueError("Invalid or expired session token")
if session_expiry(session) < datetime.now(timezone.utc):
await db.instance.delete_session(session.key)
raise ValueError("Invalid or expired session token")
if host is not None:
normalized_host = hostutil.normalize_host(host)
if not normalized_host:
raise ValueError("Invalid host")
current = session.host
if current is None:
if session and session_expiry(session) >= datetime.now(timezone.utc):
if session.host is None:
# First time binding: store exact host:port (or IPv6 form) now.
await db.instance.set_session_host(session.key, normalized_host)
session.host = normalized_host
elif current == normalized_host:
pass # exact match ok
else:
raise ValueError("Invalid or expired session token")
return session
await db.instance.set_session_host(session.key, host)
session.host = host
elif session.host != host:
raise ValueError("Session host mismatch")
return session
raise ValueError("Your session has expired. Please sign in again!")
async def refresh_session_token(token: str, *, ip: str, user_agent: str):
+1 -1
View File
@@ -439,7 +439,7 @@ class DB(DatabaseInterface):
credential_model = result.scalar_one_or_none()
if not credential_model:
raise ValueError("Credential not registered")
raise ValueError("This passkey is not registered with this service")
return Credential(
uuid=UUID(bytes=credential_model.uuid),
credential_id=credential_model.credential_id,
+12 -3
View File
@@ -71,11 +71,15 @@ async def websocket_register_add(
host = origin.split("://", 1)[1]
if reset is not None:
if not passphrase.is_well_formed(reset):
raise ValueError("Invalid reset token")
raise ValueError(
f"The reset link for {passkey.instance.rp_name} is invalid or has expired"
)
s = await get_reset(reset)
else:
if not auth:
raise ValueError("Authentication Required")
raise ValueError(
f"You must be signed in to {passkey.instance.rp_name} to add a new passkey"
)
s = await get_session(auth, host=host)
user_uuid = s.user_uuid
@@ -127,7 +131,12 @@ async def websocket_authenticate(ws: WebSocket):
# Wait for the client to use his authenticator to authenticate
credential = passkey.instance.auth_parse(await ws.receive_json())
# Fetch from the database by credential ID
stored_cred = await db.instance.get_credential_by_id(credential.raw_id)
try:
stored_cred = await db.instance.get_credential_by_id(credential.raw_id)
except ValueError:
raise ValueError(
f"This passkey is no longer registered with {passkey.instance.rp_name}"
)
# Verify the credential matches the stored data
passkey.instance.auth_verify(credential, challenge, stored_cred, origin=origin)
# Update both credential and user's last_seen timestamp