Better error messages from backend, avoid bad toasts, cleanup of session validation.
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user