diff --git a/crates/nvisy-cli/src/main.rs b/crates/nvisy-cli/src/main.rs index 0cb244f..f58c231 100644 --- a/crates/nvisy-cli/src/main.rs +++ b/crates/nvisy-cli/src/main.rs @@ -10,8 +10,7 @@ use std::process; use axum::Router; use nvisy_server::handler::{CustomRoutes, routes}; use nvisy_server::middleware::*; -use nvisy_server::service::ServiceState; -use nvisy_server::worker::WebhookWorker; +use nvisy_server::service::{ServiceState, WebhookWorker}; use tokio_util::sync::CancellationToken; use crate::config::{Cli, MiddlewareConfig}; diff --git a/crates/nvisy-server/src/handler/accounts.rs b/crates/nvisy-server/src/handler/accounts.rs index 34c107c..d4f09a2 100644 --- a/crates/nvisy-server/src/handler/accounts.rs +++ b/crates/nvisy-server/src/handler/accounts.rs @@ -17,7 +17,7 @@ use uuid::Uuid; use super::request::{AccountPathParams, UpdateAccount}; use super::response::{Account, ErrorResponse}; use crate::extract::{AuthState, Json, Path, ValidateJson}; -use crate::handler::{ErrorKind, Result}; +use crate::handler::{Error, ErrorKind, Result}; use crate::service::{PasswordHasher, PasswordStrength, ServiceState}; /// Tracing target for account operations. @@ -183,11 +183,7 @@ async fn delete_own_account( let mut conn = pg_client.get_connection().await?; conn.delete_account(auth_claims.account_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("Account not found.") - .with_resource("account") - })?; + .ok_or_else(|| Error::not_found("account"))?; tracing::info!(target: TRACING_TARGET, "Account deleted"); @@ -211,11 +207,9 @@ fn build_password_user_inputs<'a>(display_name: &'a str, email_address: &'a str) /// Finds an account by ID or returns NotFound error. async fn find_account(conn: &mut PgConn, account_id: Uuid) -> Result { - conn.find_account_by_id(account_id).await?.ok_or_else(|| { - ErrorKind::NotFound - .with_message("Account not found") - .with_resource("account") - }) + conn.find_account_by_id(account_id) + .await? + .ok_or_else(|| Error::not_found("account")) } /// Returns a [`Router`] with all related routes. diff --git a/crates/nvisy-server/src/handler/annotations.rs b/crates/nvisy-server/src/handler/annotations.rs index b1b033d..946ffd6 100644 --- a/crates/nvisy-server/src/handler/annotations.rs +++ b/crates/nvisy-server/src/handler/annotations.rs @@ -16,7 +16,7 @@ use crate::handler::request::{ AnnotationPathParams, CreateAnnotation, CursorPagination, FilePathParams, UpdateAnnotation, }; use crate::handler::response::{Annotation, AnnotationsPage, ErrorResponse}; -use crate::handler::{ErrorKind, Result}; +use crate::handler::{Error, ErrorKind, Result}; use crate::service::ServiceState; /// Tracing target for annotation operations. @@ -29,22 +29,14 @@ async fn find_annotation( ) -> Result { conn.find_workspace_file_annotation_by_id(annotation_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("Annotation not found") - .with_resource("annotation") - }) + .ok_or_else(|| Error::not_found("annotation")) } /// Finds a file by ID or returns NotFound error. async fn find_file(conn: &mut PgConn, file_id: Uuid) -> Result { conn.find_workspace_file_by_id(file_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("File not found") - .with_resource("file") - }) + .ok_or_else(|| Error::not_found("file")) } /// Creates a new annotation on a file. diff --git a/crates/nvisy-server/src/handler/authentication.rs b/crates/nvisy-server/src/handler/authentication.rs index fb8d1fa..102a1e8 100644 --- a/crates/nvisy-server/src/handler/authentication.rs +++ b/crates/nvisy-server/src/handler/authentication.rs @@ -75,7 +75,7 @@ async fn login( }; // Check for login failures and return appropriate errors - match &account { + let account = match account { None => { tracing::warn!(target: TRACING_TARGET, reason = "account_not_found", "Login failed"); return Err(ErrorKind::Unauthorized @@ -100,10 +100,9 @@ async fn login( .with_resource("account") .with_message("Account has been deleted")); } - _ => {} - } + Some(acc) => acc, + }; - let account = account.unwrap(); // Safe because we verified above let expired_at = Timestamp::now() + Span::new().hours(90 * 24); let new_token = NewAccountApiToken { account_id: account.id, diff --git a/crates/nvisy-server/src/handler/connections.rs b/crates/nvisy-server/src/handler/connections.rs index 7efe127..c0fca8a 100644 --- a/crates/nvisy-server/src/handler/connections.rs +++ b/crates/nvisy-server/src/handler/connections.rs @@ -28,7 +28,7 @@ use crate::handler::request::{ WorkspacePathParams, }; use crate::handler::response::{Connection, ConnectionsPage, ErrorResponse}; -use crate::handler::{ErrorKind, Result}; +use crate::handler::{Error, ErrorKind, Result}; use crate::service::crypto::encrypt_json; use crate::service::{MasterKey, ServiceState}; @@ -341,11 +341,7 @@ fn delete_connection_docs(op: TransformOperation) -> TransformOperation { async fn find_connection(conn: &mut PgConn, connection_id: Uuid) -> Result { conn.find_workspace_connection_by_id(connection_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("Connection not found") - .with_resource("connection") - }) + .ok_or_else(|| Error::not_found("connection")) } /// Returns routes for workspace connection management. diff --git a/crates/nvisy-server/src/handler/contexts.rs b/crates/nvisy-server/src/handler/contexts.rs index a45e4d2..07b0816 100644 --- a/crates/nvisy-server/src/handler/contexts.rs +++ b/crates/nvisy-server/src/handler/contexts.rs @@ -20,7 +20,7 @@ use uuid::Uuid; use crate::extract::{AuthProvider, AuthState, Json, Multipart, Path, Permission, Query}; use crate::handler::request::{ContextPathParams, CursorPagination, WorkspacePathParams}; use crate::handler::response::{Context, ContextsPage, ErrorResponse}; -use crate::handler::{ErrorKind, Result}; +use crate::handler::{Error, ErrorKind, Result}; use crate::middleware::DEFAULT_MAX_FILE_BODY_SIZE; use crate::service::crypto::encrypt; use crate::service::{MasterKey, ServiceState}; @@ -461,11 +461,7 @@ fn delete_context_docs(op: TransformOperation) -> TransformOperation { async fn find_context(conn: &mut PgConn, context_id: Uuid) -> Result { conn.find_workspace_context_by_id(context_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("Context not found") - .with_resource("context") - }) + .ok_or_else(|| Error::not_found("context")) } /// Returns routes for workspace context management. diff --git a/crates/nvisy-server/src/handler/error/http_error.rs b/crates/nvisy-server/src/handler/error/http_error.rs index a679c57..1765ba9 100644 --- a/crates/nvisy-server/src/handler/error/http_error.rs +++ b/crates/nvisy-server/src/handler/error/http_error.rs @@ -40,6 +40,13 @@ impl Error<'static> { suggestion: None, } } + + /// Creates a [`NotFound`](ErrorKind::NotFound) error for the given resource. + pub fn not_found(resource: &'static str) -> Self { + Self::new(ErrorKind::NotFound) + .with_message(format!("{resource} not found")) + .with_resource(resource) + } } impl<'a> Error<'a> { diff --git a/crates/nvisy-server/src/handler/files.rs b/crates/nvisy-server/src/handler/files.rs index 2c0d2d4..4fe4caf 100644 --- a/crates/nvisy-server/src/handler/files.rs +++ b/crates/nvisy-server/src/handler/files.rs @@ -12,7 +12,7 @@ use aide::transform::TransformOperation; use axum::body::Body; use axum::extract::multipart::Field; use axum::extract::{DefaultBodyLimit, State}; -use axum::http::{HeaderMap, StatusCode}; +use axum::http::{HeaderMap, HeaderValue, StatusCode}; use futures::StreamExt; use nvisy_nats::NatsClient; use nvisy_nats::object::{FileKey, FilesBucket, ObjectStore}; @@ -30,7 +30,7 @@ use crate::handler::request::{ CursorPagination, FilePathParams, ListFiles, UpdateFile, WorkspacePathParams, }; use crate::handler::response::{self, ErrorResponse, File, Files, FilesPage}; -use crate::handler::{ErrorKind, Result}; +use crate::handler::{Error, ErrorKind, Result}; use crate::middleware::DEFAULT_MAX_FILE_BODY_SIZE; use crate::service::{ServiceState, WebhookEmitter}; @@ -44,11 +44,7 @@ type FileJobPublisher = EventPublisher, FileStream>; async fn find_file(conn: &mut PgConn, file_id: Uuid) -> Result { conn.find_workspace_file_by_id(file_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("File not found") - .with_resource("file") - }) + .ok_or_else(|| Error::not_found("file")) } /// Lists files in a workspace with cursor-based pagination. @@ -476,15 +472,22 @@ async fn download_file( ErrorKind::NotFound.with_message("File content not found") })?; - // Set up response headers - let mut headers = HeaderMap::new(); + // Set up response headers. + // + // The display name is user-controlled, so strip characters that are + // invalid in a quoted header value to avoid header injection and a + // failed parse; fall back to a generic disposition if it still fails. + let safe_name: String = file + .display_name + .chars() + .filter(|c| !c.is_control() && *c != '"' && *c != '\\') + .collect(); + let disposition = format!("attachment; filename=\"{safe_name}\"") + .parse() + .unwrap_or_else(|_| HeaderValue::from_static("attachment")); - headers.insert( - "content-disposition", - format!("attachment; filename=\"{}\"", file.display_name) - .parse() - .unwrap(), - ); + let mut headers = HeaderMap::new(); + headers.insert("content-disposition", disposition); headers.insert( "content-length", get_result.size().to_string().parse().unwrap(), diff --git a/crates/nvisy-server/src/handler/webhooks.rs b/crates/nvisy-server/src/handler/webhooks.rs index 350b887..cb493ac 100644 --- a/crates/nvisy-server/src/handler/webhooks.rs +++ b/crates/nvisy-server/src/handler/webhooks.rs @@ -25,7 +25,7 @@ use crate::handler::request::{ use crate::handler::response::{ ErrorResponse, Webhook, WebhookCreated, WebhookResult, WebhooksPage, }; -use crate::handler::{ErrorKind, Result}; +use crate::handler::{Error, ErrorKind, Result}; use crate::service::ServiceState; /// Tracing target for workspace webhook operations. @@ -340,11 +340,7 @@ fn test_webhook_docs(op: TransformOperation) -> TransformOperation { async fn find_webhook(conn: &mut PgConn, webhook_id: Uuid) -> Result { conn.find_workspace_webhook_by_id(webhook_id) .await? - .ok_or_else(|| { - ErrorKind::NotFound - .with_message("Webhook not found") - .with_resource("webhook") - }) + .ok_or_else(|| Error::not_found("webhook")) } /// Returns routes for workspace webhook management. diff --git a/crates/nvisy-server/src/lib.rs b/crates/nvisy-server/src/lib.rs index 40b5661..9987e42 100644 --- a/crates/nvisy-server/src/lib.rs +++ b/crates/nvisy-server/src/lib.rs @@ -8,6 +8,5 @@ pub mod extract; pub mod handler; pub mod middleware; pub mod service; -pub mod worker; pub use crate::error::{BoxedError, Error, ErrorKind, Result}; diff --git a/crates/nvisy-server/src/service/mod.rs b/crates/nvisy-server/src/service/mod.rs index 0e81303..6ec0824 100644 --- a/crates/nvisy-server/src/service/mod.rs +++ b/crates/nvisy-server/src/service/mod.rs @@ -17,7 +17,7 @@ pub use crate::service::security::{ MasterKey, MasterKeyConfig, PasswordHasher, PasswordStrength, SessionKeys, SessionKeysConfig, UserAgentParser, }; -pub use crate::service::webhook::WebhookEmitter; +pub use crate::service::webhook::{WebhookEmitter, WebhookWorker}; use crate::{Error, Result}; /// Application state. diff --git a/crates/nvisy-server/src/service/webhook/mod.rs b/crates/nvisy-server/src/service/webhook/mod.rs index 7c3ddf6..dcbe839 100644 --- a/crates/nvisy-server/src/service/webhook/mod.rs +++ b/crates/nvisy-server/src/service/webhook/mod.rs @@ -1,7 +1,11 @@ -//! Webhook event emission service. +//! Webhook event emission and delivery services. //! -//! Provides helpers for emitting domain events to webhooks via NATS JetStream. +//! Provides helpers for emitting domain events to webhooks via NATS JetStream +//! ([`WebhookEmitter`]) and the background worker that delivers them +//! ([`WebhookWorker`]). mod emitter; +mod worker; pub use emitter::WebhookEmitter; +pub use worker::WebhookWorker; diff --git a/crates/nvisy-server/src/worker/webhook.rs b/crates/nvisy-server/src/service/webhook/worker.rs similarity index 100% rename from crates/nvisy-server/src/worker/webhook.rs rename to crates/nvisy-server/src/service/webhook/worker.rs diff --git a/crates/nvisy-server/src/worker/mod.rs b/crates/nvisy-server/src/worker/mod.rs deleted file mode 100644 index 4e558cf..0000000 --- a/crates/nvisy-server/src/worker/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Background workers for async processing. - -mod webhook; - -pub use webhook::WebhookWorker;