diff --git a/src/application/routes/account.rs b/src/application/routes/account.rs index d7e3010..5afa2b7 100644 --- a/src/application/routes/account.rs +++ b/src/application/routes/account.rs @@ -6,6 +6,7 @@ use axum::response::{IntoResponse, Redirect, Response}; use chrono::{DateTime, Utc}; use serde::Serialize; use tower_cookies::Cookies; +use tracing::{error, info, warn}; use crate::application::auth::AuthenticatedUser; use crate::application::routes::render_html; @@ -98,7 +99,10 @@ pub(crate) async fn account_page( .passkey_repo .list_by_user(auth_user.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map_err(|err| { + error!(error = %err, "failed to list passkeys for account page"); + StatusCode::INTERNAL_SERVER_ERROR + })? .into_iter() .map(|p| PasskeyView { id: i64::from(p.id), @@ -112,7 +116,10 @@ pub(crate) async fn account_page( .token_repo .list_by_user(auth_user.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map_err(|err| { + error!(error = %err, "failed to list tokens for account page"); + StatusCode::INTERNAL_SERVER_ERROR + })? .into_iter() .filter(crate::domain::tokens::Token::is_active) .map(|t| TokenView { @@ -123,17 +130,18 @@ pub(crate) async fn account_page( }) .collect(); - let ai_usage = state - .ai_usage_repo - .summary_for_user(auth_user.id) - .await - .ok() - .filter(|s| s.total_calls > 0) - .map(|s| AiUsageView { - total_calls: s.total_calls, - total_tokens: format_number(s.total_tokens), - total_cost: format_cost(s.total_cost), - }); + let ai_usage = match state.ai_usage_repo.summary_for_user(auth_user.id).await { + Ok(summary) => Some(summary), + Err(err) => { + warn!(error = %err, "failed to load AI usage summary"); + None + } + }; + let ai_usage = ai_usage.filter(|s| s.total_calls > 0).map(|s| AiUsageView { + total_calls: s.total_calls, + total_tokens: format_number(s.total_tokens), + total_cost: format_cost(s.total_cost), + }); let template = AccountTemplate { nav_active: "account", @@ -165,7 +173,10 @@ pub(crate) async fn list_passkeys( .passkey_repo .list_by_user(auth_user.0.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, "failed to list passkeys"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let responses: Vec = passkeys .into_iter() @@ -186,11 +197,10 @@ pub(crate) async fn delete_passkey( Path(passkey_id): Path, ) -> Result { // Verify the passkey belongs to the user - let passkey = state - .passkey_repo - .get(passkey_id) - .await - .map_err(|_| StatusCode::NOT_FOUND)?; + let passkey = state.passkey_repo.get(passkey_id).await.map_err(|err| { + error!(error = %err, %passkey_id, "failed to get passkey for deletion"); + StatusCode::NOT_FOUND + })?; if passkey.user_id != auth_user.0.id { return Err(StatusCode::FORBIDDEN); @@ -201,17 +211,21 @@ pub(crate) async fn delete_passkey( .passkey_repo .list_by_user(auth_user.0.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, "failed to list passkeys for deletion check"); + StatusCode::INTERNAL_SERVER_ERROR + })?; if all_passkeys.len() <= 1 { return Err(StatusCode::CONFLICT); } - state - .passkey_repo - .delete(passkey_id) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + state.passkey_repo.delete(passkey_id).await.map_err(|err| { + error!(error = %err, %passkey_id, "failed to delete passkey"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + info!(%passkey_id, user_id = %auth_user.0.id, "passkey deleted"); Ok(StatusCode::NO_CONTENT) } @@ -226,15 +240,27 @@ async fn extract_user_from_session( let session_token = cookie.value(); let session_token_hash = crate::infrastructure::auth::hash_token(session_token); - let session = state + let session = match state .session_repo .get_by_token_hash(&session_token_hash) .await - .ok()?; + { + Ok(s) => s, + Err(err) => { + warn!(error = %err, "session lookup failed on account page"); + return None; + } + }; if session.is_expired() { return None; } - state.user_repo.get(session.user_id).await.ok() + match state.user_repo.get(session.user_id).await { + Ok(user) => Some(user), + Err(err) => { + warn!(error = %err, user_id = %session.user_id, "user lookup failed for valid session"); + None + } + } } diff --git a/src/application/routes/auth.rs b/src/application/routes/auth.rs index ccc2a0f..ea91a2a 100644 --- a/src/application/routes/auth.rs +++ b/src/application/routes/auth.rs @@ -4,6 +4,7 @@ use axum::http::StatusCode; use axum::response::{IntoResponse, Redirect, Response}; use serde::Deserialize; use tower_cookies::{Cookie, Cookies}; +use tracing::{info, warn}; use crate::application::routes::render_html; use crate::application::server::AppState; @@ -55,11 +56,13 @@ pub(crate) async fn logout(State(state): State, cookies: Cookies) -> R .session_repo .get_by_token_hash(&session_token_hash) .await + && let Err(err) = state.session_repo.delete(session.id).await { - let _ = state.session_repo.delete(session.id).await; + warn!(error = %err, session_id = %session.id, "failed to delete session on logout"); } } + info!("user logged out"); cookies.remove(Cookie::from(SESSION_COOKIE_NAME)); Redirect::to("/") } @@ -79,6 +82,9 @@ pub async fn is_authenticated(state: &AppState, cookies: &Cookies) -> bool { .await { Ok(session) => !session.is_expired(), - Err(_) => false, + Err(err) => { + warn!(error = %err, "session lookup failed during auth check"); + false + } } } diff --git a/src/application/routes/tokens.rs b/src/application/routes/tokens.rs index 1ca14da..5d62b30 100644 --- a/src/application/routes/tokens.rs +++ b/src/application/routes/tokens.rs @@ -3,6 +3,7 @@ use axum::extract::{Path, State}; use axum::http::StatusCode; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use tracing::{error, info}; use crate::application::auth::AuthenticatedUser; use crate::application::server::AppState; @@ -51,16 +52,20 @@ pub async fn create_token( auth_user: AuthenticatedUser, Json(payload): Json, ) -> Result, StatusCode> { - let token_value = generate_token().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let token_value = generate_token().map_err(|err| { + error!(error = %err, "failed to generate token"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let token_hash_value = hash_token(&token_value); let new_token = NewToken::new(auth_user.0.id, token_hash_value, payload.name.clone()); - let stored_token = state - .token_repo - .insert(new_token) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let stored_token = state.token_repo.insert(new_token).await.map_err(|err| { + error!(error = %err, "failed to store token"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + info!(token_id = %stored_token.id, token_name = %stored_token.name, user_id = %auth_user.0.id, "API token created"); Ok(Json(CreateTokenResponse { id: stored_token.id, @@ -78,7 +83,10 @@ pub async fn list_tokens( .token_repo .list_by_user(auth_user.0.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, "failed to list tokens"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let token_responses: Vec = tokens.into_iter().map(TokenResponse::from).collect(); @@ -92,11 +100,10 @@ pub async fn revoke_token( Path(token_id): Path, ) -> Result, StatusCode> { // Get the token to ensure it exists and belongs to the user - let token = state - .token_repo - .get(token_id) - .await - .map_err(|_| StatusCode::NOT_FOUND)?; + let token = state.token_repo.get(token_id).await.map_err(|err| { + error!(error = %err, %token_id, "failed to get token for revocation"); + StatusCode::NOT_FOUND + })?; // Ensure the token belongs to the authenticated user if token.user_id != auth_user.0.id { @@ -104,11 +111,12 @@ pub async fn revoke_token( } // Revoke the token - let revoked_token = state - .token_repo - .revoke(token_id) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let revoked_token = state.token_repo.revoke(token_id).await.map_err(|err| { + error!(error = %err, %token_id, "failed to revoke token"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + info!(%token_id, user_id = %auth_user.0.id, "API token revoked"); Ok(Json(TokenResponse::from(revoked_token))) } diff --git a/src/application/routes/webauthn.rs b/src/application/routes/webauthn.rs index b5ccde5..e0a6f72 100644 --- a/src/application/routes/webauthn.rs +++ b/src/application/routes/webauthn.rs @@ -138,7 +138,10 @@ pub(crate) async fn register_start( .registration_token_repo .get_by_token_hash(&token_hash) .await - .map_err(|_| StatusCode::UNAUTHORIZED)?; + .map_err(|err| { + warn!(error = %err, "registration token lookup failed"); + StatusCode::UNAUTHORIZED + })?; if !reg_token.is_valid() { return Err(StatusCode::GONE); @@ -153,14 +156,19 @@ pub(crate) async fn register_start( })?; // Mark registration token as used - let _ = state + if let Err(err) = state .registration_token_repo .mark_used(reg_token.id, user.id) - .await; + .await + { + warn!(error = %err, token_id = %reg_token.id, "failed to mark registration token as used"); + } // Start passkey registration ceremony - let webauthn_uuid = - Uuid::parse_str(&user_uuid).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let webauthn_uuid = Uuid::parse_str(&user_uuid).map_err(|err| { + error!(error = %err, "failed to parse user UUID"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let exclude_credentials = Vec::new(); let (ccr, reg_state) = state @@ -214,8 +222,10 @@ pub(crate) async fn register_finish( })?; // Store the credential - let credential_json = - serde_json::to_string(&passkey).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let credential_json = serde_json::to_string(&passkey).map_err(|err| { + error!(error = %err, "failed to serialize passkey credential"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.passkey_name); state .passkey_repo @@ -242,11 +252,10 @@ pub(crate) async fn auth_start( Query(query): Query, ) -> Result, StatusCode> { // Load all passkey credentials from all users - let users = state - .user_repo - .list_all() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let users = state.user_repo.list_all().await.map_err(|err| { + error!(error = %err, "failed to list users for auth start"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let mut all_passkeys: Vec = Vec::new(); for user in &users { @@ -254,7 +263,10 @@ pub(crate) async fn auth_start( .passkey_repo .list_by_user(user.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, user_id = %user.id, "failed to list passkeys for user"); + StatusCode::INTERNAL_SERVER_ERROR + })?; for cred in credentials { let passkey: Passkey = serde_json::from_str(&cred.credential_json).map_err(|err| { @@ -300,6 +312,7 @@ pub(crate) async fn auth_start( // --- Authentication finish --- +#[allow(clippy::too_many_lines)] #[tracing::instrument(skip(state, cookies, payload))] pub(crate) async fn auth_finish( State(state): State, @@ -324,11 +337,10 @@ pub(crate) async fn auth_finish( // Find the user who owns this credential let credential_id = auth_result.cred_id(); - let users = state - .user_repo - .list_all() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let users = state.user_repo.list_all().await.map_err(|err| { + error!(error = %err, "failed to list users for credential lookup"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let mut found_user_id = None; let mut found_cred_id = None; @@ -339,11 +351,17 @@ pub(crate) async fn auth_finish( .passkey_repo .list_by_user(user.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, user_id = %user.id, "failed to list passkeys for user"); + StatusCode::INTERNAL_SERVER_ERROR + })?; for cred in &credentials { let passkey: Passkey = serde_json::from_str(&cred.credential_json) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, credential_id = %cred.id, "failed to deserialize passkey credential"); + StatusCode::INTERNAL_SERVER_ERROR + })?; if passkey.cred_id() == credential_id { found_user_id = Some(user.id); found_cred_id = Some(cred.id); @@ -361,31 +379,45 @@ pub(crate) async fn auth_finish( && let Some(mut passkey) = found_passkey { passkey.update_credential(&auth_result); - if let Ok(updated_json) = serde_json::to_string(&passkey) { - let _ = state - .passkey_repo - .update_credential_json(cred_db_id, &updated_json) - .await; + match serde_json::to_string(&passkey) { + Ok(updated_json) => { + if let Err(err) = state + .passkey_repo + .update_credential_json(cred_db_id, &updated_json) + .await + { + warn!(error = %err, credential_id = %cred_db_id, "failed to update passkey credential counter"); + } + } + Err(err) => { + warn!(error = %err, "failed to serialize updated passkey credential"); + } } } // Update last used timestamp let passkey_repo = state.passkey_repo.clone(); tokio::spawn(async move { - let _ = passkey_repo.update_last_used(cred_db_id).await; + if let Err(err) = passkey_repo.update_last_used(cred_db_id).await { + warn!(error = %err, credential_id = %cred_db_id, "failed to update passkey last_used"); + } }); // Handle CLI callback flow if let Some(cli_info) = cli_callback { // Generate a bearer token for the CLI - let token_value = generate_token().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let token_value = generate_token().map_err(|err| { + error!(error = %err, "failed to generate CLI bearer token"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let token_hash_value = hash_token(&token_value); let new_token = NewToken::new(user_id, token_hash_value, cli_info.token_name); - state - .token_repo - .insert(new_token) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + state.token_repo.insert(new_token).await.map_err(|err| { + error!(error = %err, "failed to store CLI bearer token"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + info!(user_id = %user_id, "CLI token created via passkey auth"); let redirect_url = format!( "{}?token={}&state={}", @@ -425,15 +457,20 @@ pub(crate) async fn passkey_add_start( Json(payload): Json, ) -> Result>, StatusCode> { let user = auth_user.0; - let webauthn_uuid = - Uuid::parse_str(&user.uuid).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let webauthn_uuid = Uuid::parse_str(&user.uuid).map_err(|err| { + error!(error = %err, "failed to parse user UUID for passkey add"); + StatusCode::INTERNAL_SERVER_ERROR + })?; // Load existing credentials to exclude (prevents re-registering same authenticator) let existing = state .passkey_repo .list_by_user(user.id) .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + .map_err(|err| { + error!(error = %err, user_id = %user.id, "failed to list existing passkeys"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let exclude_credentials = existing .iter() @@ -485,8 +522,10 @@ pub(crate) async fn passkey_add_finish( StatusCode::BAD_REQUEST })?; - let credential_json = - serde_json::to_string(&passkey).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let credential_json = serde_json::to_string(&passkey).map_err(|err| { + error!(error = %err, "failed to serialize new passkey credential"); + StatusCode::INTERNAL_SERVER_ERROR + })?; let new_credential = NewPasskeyCredential::new(user_id, credential_json, payload.name); state .passkey_repo