diff --git a/.gitignore b/.gitignore index 68b92ba1..016db605 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ target/ .vscode/ test/ .env +nr_tests.env diff --git a/Cargo.lock b/Cargo.lock index e6ee2e3f..b1287906 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2088,6 +2088,7 @@ dependencies = [ name = "nr-core" version = "0.1.0" dependencies = [ + "anyhow", "badge-maker", "base64 0.22.1", "chrono", @@ -2106,7 +2107,10 @@ dependencies = [ "sqlx", "strum 0.26.3", "thiserror", + "tokio", "tracing", + "tracing-appender", + "tracing-subscriber", "url", "utoipa", "uuid", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 71816c8c..12a972b8 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -26,5 +26,24 @@ derive_more.workspace = true digestible.workspace = true url.workspace = true nr-macros.workspace = true + +# Testing +anyhow = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", + "json", +], optional = true } +tracing-appender = { version = "0.2", optional = true } +[features] +default = ["migrations", "testing"] +migrations = [] +testing = [ + "migrations", + "anyhow", + "tokio", + "tracing-subscriber", + "tracing-appender", +] [lints] workspace = true diff --git a/nitro_repo/migrations/20240727231504_ignore_case.down.sql b/crates/core/migrations/20240727231504_ignore_case.down.sql similarity index 100% rename from nitro_repo/migrations/20240727231504_ignore_case.down.sql rename to crates/core/migrations/20240727231504_ignore_case.down.sql diff --git a/nitro_repo/migrations/20240727231504_ignore_case.up.sql b/crates/core/migrations/20240727231504_ignore_case.up.sql similarity index 100% rename from nitro_repo/migrations/20240727231504_ignore_case.up.sql rename to crates/core/migrations/20240727231504_ignore_case.up.sql diff --git a/nitro_repo/migrations/20240823113130_create_storages_and_repositories.down.sql b/crates/core/migrations/20240823113130_create_storages_and_repositories.down.sql similarity index 100% rename from nitro_repo/migrations/20240823113130_create_storages_and_repositories.down.sql rename to crates/core/migrations/20240823113130_create_storages_and_repositories.down.sql diff --git a/nitro_repo/migrations/20240823113130_create_storages_and_repositories.up.sql b/crates/core/migrations/20240823113130_create_storages_and_repositories.up.sql similarity index 100% rename from nitro_repo/migrations/20240823113130_create_storages_and_repositories.up.sql rename to crates/core/migrations/20240823113130_create_storages_and_repositories.up.sql diff --git a/nitro_repo/migrations/20240823113222_users.down.sql b/crates/core/migrations/20240823113222_users.down.sql similarity index 100% rename from nitro_repo/migrations/20240823113222_users.down.sql rename to crates/core/migrations/20240823113222_users.down.sql diff --git a/nitro_repo/migrations/20240823113222_users.up.sql b/crates/core/migrations/20240823113222_users.up.sql similarity index 100% rename from nitro_repo/migrations/20240823113222_users.up.sql rename to crates/core/migrations/20240823113222_users.up.sql diff --git a/nitro_repo/migrations/20240823113259_user_extras.down.sql b/crates/core/migrations/20240823113259_user_extras.down.sql similarity index 100% rename from nitro_repo/migrations/20240823113259_user_extras.down.sql rename to crates/core/migrations/20240823113259_user_extras.down.sql diff --git a/nitro_repo/migrations/20240823113259_user_extras.up.sql b/crates/core/migrations/20240823113259_user_extras.up.sql similarity index 100% rename from nitro_repo/migrations/20240823113259_user_extras.up.sql rename to crates/core/migrations/20240823113259_user_extras.up.sql diff --git a/nitro_repo/migrations/20240823113321_user_auth_tokens.down.sql b/crates/core/migrations/20240823113321_user_auth_tokens.down.sql similarity index 100% rename from nitro_repo/migrations/20240823113321_user_auth_tokens.down.sql rename to crates/core/migrations/20240823113321_user_auth_tokens.down.sql diff --git a/nitro_repo/migrations/20240823113321_user_auth_tokens.up.sql b/crates/core/migrations/20240823113321_user_auth_tokens.up.sql similarity index 100% rename from nitro_repo/migrations/20240823113321_user_auth_tokens.up.sql rename to crates/core/migrations/20240823113321_user_auth_tokens.up.sql diff --git a/nitro_repo/migrations/20240823113359_create_stages.down.sql b/crates/core/migrations/20240823113359_create_stages.down.sql similarity index 100% rename from nitro_repo/migrations/20240823113359_create_stages.down.sql rename to crates/core/migrations/20240823113359_create_stages.down.sql diff --git a/nitro_repo/migrations/20240823113359_create_stages.up.sql b/crates/core/migrations/20240823113359_create_stages.up.sql similarity index 100% rename from nitro_repo/migrations/20240823113359_create_stages.up.sql rename to crates/core/migrations/20240823113359_create_stages.up.sql diff --git a/nitro_repo/migrations/20240823113416_create_projects.down.sql b/crates/core/migrations/20240823113416_create_projects.down.sql similarity index 100% rename from nitro_repo/migrations/20240823113416_create_projects.down.sql rename to crates/core/migrations/20240823113416_create_projects.down.sql diff --git a/nitro_repo/migrations/20240823113416_create_projects.up.sql b/crates/core/migrations/20240823113416_create_projects.up.sql similarity index 100% rename from nitro_repo/migrations/20240823113416_create_projects.up.sql rename to crates/core/migrations/20240823113416_create_projects.up.sql diff --git a/crates/core/src/database/migration.rs b/crates/core/src/database/migration.rs new file mode 100644 index 00000000..1f268513 --- /dev/null +++ b/crates/core/src/database/migration.rs @@ -0,0 +1,6 @@ +use sqlx::{migrate::Migrator, PgPool}; +static MIGRATOR: Migrator = sqlx::migrate!(); +pub async fn run_migrations(pool: &PgPool) -> Result<(), sqlx::Error> { + MIGRATOR.run(pool).await?; + Ok(()) +} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 9e03e44c..fdcb3e6a 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -1,6 +1,5 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - +#[cfg(feature = "migrations")] +pub mod migration; pub mod project; pub mod repository; pub mod storage; diff --git a/crates/core/src/database/user/mod.rs b/crates/core/src/database/user/mod.rs index 31e44aee..df3670ac 100644 --- a/crates/core/src/database/user/mod.rs +++ b/crates/core/src/database/user/mod.rs @@ -353,7 +353,7 @@ impl NewUserRequest { password, } = self; let user = sqlx::query_as( - r#"INSERT INTO users (name, username, email, password, admin, user_manager, storage_manager, repository_manager) VALUES ($1, $2, $3, $4, true, true, true, true) RETURNING *"#, + r#"INSERT INTO users (name, username, email, password, admin, user_manager, system_manager) VALUES ($1, $2, $3, $4, true, true, true) RETURNING *"#, ) .bind(name) .bind(username) diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 7d75095c..baabafc3 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -4,4 +4,6 @@ pub mod builder_error; pub mod database; pub mod repository; pub mod storage; +#[cfg(feature = "testing")] +pub mod testing; pub mod utils; diff --git a/crates/core/src/repository/config.rs b/crates/core/src/repository/config.rs index 55a55c3a..00e0e71b 100644 --- a/crates/core/src/repository/config.rs +++ b/crates/core/src/repository/config.rs @@ -55,10 +55,12 @@ pub trait RepositoryConfigType: Send + Sync + Debug { ..Default::default() } } - /// Sanitizes the config for public view. By default this function returns None which will mean the config is not shown to the public + /// Sanitizes the config for public view. + /// + /// By default this function returns None which will mean the config is not shown to the public #[inline(always)] - fn sanitize_for_public_view(&self, _: Value) -> Option { - None + fn sanitize_for_public_view(&self, _: Value) -> Result, RepositoryConfigError> { + Ok(None) } /// Validate the config. If the config is invalid this function should return an error fn validate_config(&self, config: Value) -> Result<(), RepositoryConfigError>; diff --git a/crates/core/src/testing/env_file.rs b/crates/core/src/testing/env_file.rs new file mode 100644 index 00000000..2ba0e3b0 --- /dev/null +++ b/crates/core/src/testing/env_file.rs @@ -0,0 +1,41 @@ +use std::{collections::HashMap, path::PathBuf}; + +use tracing::{debug, info, instrument}; +#[instrument] +pub fn find_file(dir: PathBuf, file_name: &str) -> Option { + let env_file = dir.join(file_name); + info!("Checking for file: {:?}", env_file); + if env_file.exists() { + return Some(env_file); + } + let parent = dir.parent()?; + debug!("Checking parent: {:?}", parent); + find_file(parent.to_path_buf(), file_name) +} +#[derive(Debug)] +pub struct EnvFile { + pub file: PathBuf, + pub key_values: HashMap, +} +impl EnvFile { + pub fn load(file_name: &str) -> anyhow::Result { + let current_dir = std::env::current_dir()?; + let file = + find_file(current_dir, file_name).ok_or_else(|| anyhow::anyhow!("File not found"))?; + let file_contents = std::fs::read_to_string(&file)?; + let mut key_values = HashMap::new(); + for line in file_contents.lines() { + let mut parts = line.splitn(2, '='); + let key = parts.next().unwrap(); + let value = parts.next().unwrap(); + key_values.insert(key.to_string(), value.to_string()); + } + Ok(Self { file, key_values }) + } + pub fn get(&self, key: &str) -> Option { + if let Ok(key) = std::env::var(key) { + return Some(key); + } + self.key_values.get(key).map(|s| s.to_owned()) + } +} diff --git a/crates/core/src/testing/mod.rs b/crates/core/src/testing/mod.rs new file mode 100644 index 00000000..1783fac6 --- /dev/null +++ b/crates/core/src/testing/mod.rs @@ -0,0 +1,143 @@ +use std::str::FromStr; + +use env_file::EnvFile; +use sqlx::PgPool; +use tracing::{debug, error, info}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; +pub mod env_file; + +use crate::{ + database::{ + user::{NewUserRequest, UserSafeData, UserType}, + DateTime, + }, + user::{Email, Username}, +}; +/// The password for the test user +pub static TEST_USER_USERNAME: &str = "test_user"; + +pub static TEST_USER_PASSWORD: &str = "password"; +static TEST_USER_PASSWORD_HASHED: &str = + "$argon2id$v=19$m=16,t=2,p=1$b1o5VWFvVFYxRTFhUUJjeA$bpK+ySI4DIDIOh4emBFTqw"; +/// Table Name: `nr_test_environment` +static TEST_INFO_TABLE: &str = include_str!("test_info.sql"); +static LOGGING_INIT: std::sync::Once = std::sync::Once::new(); + +pub struct TestCore { + pub db: PgPool, +} +impl TestCore { + pub async fn new(function_path: String) -> anyhow::Result<(Self, TestInfoEntry)> { + let env_file = env_file::EnvFile::load("nr_tests.env")?; + Self::start_logger(&env_file); + let database = Self::connect(&env_file).await?; + let new = Self { db: database }; + new.init_test_environment().await?; + + let entry = TestInfoEntry::get_or_create(&function_path, &new.db).await?; + Ok((new, entry)) + } + fn start_logger(env_file: &EnvFile) { + let log = env_file.get("LOG"); + if let Some(log) = log { + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| log.into()); + + LOGGING_INIT.call_once(|| { + let stdout_log = tracing_subscriber::fmt::layer().pretty(); + match tracing_subscriber::registry() + .with(stdout_log.with_filter(env_filter)) + .try_init() + { + Ok(_) => { + debug!("Logging initialized"); + } + Err(err) => { + eprintln!("Error initializing logging: {}", err); + } + }; + }); + } + } + async fn connect(env_file: &EnvFile) -> anyhow::Result { + let env = env_file.get("DATABASE_URL").unwrap(); + debug!("Connecting to database {}", env); + let db = PgPool::connect(&env).await?; + Ok(db) + } + async fn init_test_environment(&self) -> anyhow::Result<()> { + crate::database::migration::run_migrations(&self.db).await?; + sqlx::query(TEST_INFO_TABLE).execute(&self.db).await?; + Ok(()) + } + + pub async fn get_test_user(&self) -> anyhow::Result> { + if let Some(user) = UserSafeData::get_by_id(1, &self.db).await? { + return Ok(Some(user)); + } else { + let user = NewUserRequest { + name: "Test User".to_string(), + username: Username::from_str(TEST_USER_USERNAME)?, + email: Email::from_str("testing@example.com")?, + password: Some(TEST_USER_PASSWORD_HASHED.to_owned()), + }; + let user = user.insert_admin(&self.db).await?; + return Ok(Some(user.into())); + } + } +} +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct TestInfoEntry { + pub id: i32, + pub function_path: String, + pub run_successfully: Option, + pub started_at: Option, +} +impl TestInfoEntry { + pub async fn get_or_create( + function_path: &str, + db: &PgPool, + ) -> Result { + let entry = sqlx::query_as::<_, TestInfoEntry>( + r#"INSERT INTO nr_test_environment (function_path) VALUES($1) ON CONFLICT (function_path) DO UPDATE + SET run_successfully = null, started_at = CURRENT_TIMESTAMP + RETURNING *;"#, + ) + .bind(function_path.to_owned()) + .fetch_one(db) + .await?; + Ok(entry) + } + + pub async fn set_success(&self, db: &PgPool) -> Result<(), sqlx::Error> { + sqlx::query(r#"UPDATE nr_test_environment SET run_successfully = true WHERE id = $1;"#) + .bind(self.id) + .execute(db) + .await?; + Ok(()) + } +} + +async fn does_table_exist(table_name: &str, db: &PgPool) -> Result { + let table_exists: bool = sqlx::query_scalar( + r#" + SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1) AS table_existence;"#, + ) + .bind(table_name) + .fetch_one(db) + .await?; + info!("Table {} exists: {}", table_name, table_exists); + Ok(table_exists) +} + +#[cfg(test)] +mod tests { + #[tokio::test] + pub async fn test_test_core() { + let (core, entry) = super::TestCore::new(format!("{}::test_test_core", module_path!())) + .await + .unwrap(); + let user = core.get_test_user().await.unwrap(); + assert!(user.is_some()); + entry.set_success(&core.db).await.unwrap(); + } +} diff --git a/crates/core/src/testing/test_info.sql b/crates/core/src/testing/test_info.sql new file mode 100644 index 00000000..e4b6098d --- /dev/null +++ b/crates/core/src/testing/test_info.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS nr_test_environment ( + id SERIAL PRIMARY KEY, + function_path TEXT NOT NULL, + constraint function_path_unique UNIQUE (function_path), + run_successfully BOOLEAN, + started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/crates/macros/src/dyn_repository_handler.rs b/crates/macros/src/dyn_repository_handler.rs index de7ce9a1..ecf5d9fe 100644 --- a/crates/macros/src/dyn_repository_handler.rs +++ b/crates/macros/src/dyn_repository_handler.rs @@ -1,13 +1,52 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Data, DeriveInput, Fields, Result}; +use syn::{ + parse::{Parse, ParseStream}, + Data, DeriveInput, Fields, Result, +}; +mod keywords { + use syn::custom_keyword; + custom_keyword!(error); +} +pub struct ContainerAttributes { + pub error: syn::Type, +} +impl Parse for ContainerAttributes { + fn parse(input: ParseStream) -> syn::Result { + let mut error: Option = None; + while !input.is_empty() { + if input.peek(syn::Token![,]) { + let _: syn::Token![,] = input.parse()?; + } + let lookahead = input.lookahead1(); + if lookahead.peek(keywords::error) { + let _ = input.parse::()?; + let _: syn::Token![=] = input.parse()?; + error = Some(input.parse()?); + } else { + return Err(lookahead.error()); + } + } + let attr = Self { + error: error.ok_or_else(|| syn::Error::new(input.span(), "Missing error opt"))?, + }; + Ok(attr) + } +} pub(crate) fn expand(derive_input: DeriveInput) -> Result { - let DeriveInput { ident, data, .. } = derive_input; + let DeriveInput { + ident, data, attrs, .. + } = derive_input; let Data::Enum(data_enum) = data else { return Err(syn::Error::new(ident.span(), "Expected an enum")); }; - + let ContainerAttributes { error } = attrs + .iter() + .find(|v: &&syn::Attribute| v.path().is_ident("repository_handler")) + .map(|v| v.parse_args::()) + .transpose()? + .ok_or_else(|| syn::Error::new(ident.span(), "Missing #[repository_handler]"))?; let mut impl_from = Vec::new(); let mut variants: Vec<_> = Vec::new(); for variant in data_enum.variants { @@ -41,6 +80,7 @@ pub(crate) fn expand(derive_input: DeriveInput) -> Result { )* impl Repository for #ident { + type Error = #error; fn get_storage(&self) -> nr_storage::DynStorage { match self { #( @@ -108,40 +148,40 @@ pub(crate) fn expand(derive_input: DeriveInput) -> Result { async fn resolve_project_and_version_for_path( &self, path: StoragePath, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.resolve_project_and_version_for_path(path).await, + #ident::#variants(variant) => variant.resolve_project_and_version_for_path(path).await.map_err(Self::Error::from), )* } } async fn handle_get( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_get(request).await, + #ident::#variants(variant) => variant.handle_get(request).await.map_err(Self::Error::from), )* } } async fn handle_post( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_post(request).await, + #ident::#variants(variant) => variant.handle_post(request).await.map_err(Self::Error::from), )* } } async fn handle_put( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_put(request).await, + #ident::#variants(variant) => variant.handle_put(request).await.map_err(Self::Error::from), )* } } @@ -149,40 +189,40 @@ pub(crate) fn expand(derive_input: DeriveInput) -> Result { async fn handle_patch( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_patch(request).await, + #ident::#variants(variant) => variant.handle_patch(request).await.map_err(Self::Error::from), )* } } async fn handle_delete( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_delete(request).await, + #ident::#variants(variant) => variant.handle_delete(request).await.map_err(Self::Error::from), )* } } async fn handle_head( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_head(request).await, + #ident::#variants(variant) => variant.handle_head(request).await.map_err(Self::Error::from), )* } } async fn handle_other( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { match self { #( - #ident::#variants(variant) => variant.handle_other(request).await, + #ident::#variants(variant) => variant.handle_other(request).await.map_err(Self::Error::from), )* } } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index d6d6744d..ae97c1af 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -14,7 +14,7 @@ pub fn repository_config(input: TokenStream) -> TokenStream { } } -#[proc_macro_derive(DynRepositoryHandler)] +#[proc_macro_derive(DynRepositoryHandler, attributes(repository_handler))] pub fn dyn_repository_handler(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); // Check if its an enum diff --git a/nitro_repo/src/app/api/repository/management.rs b/nitro_repo/src/app/api/repository/management.rs index 7fb10096..e2594871 100644 --- a/nitro_repo/src/app/api/repository/management.rs +++ b/nitro_repo/src/app/api/repository/management.rs @@ -9,17 +9,18 @@ use axum::{ use http::{header::CONTENT_TYPE, StatusCode}; use nr_core::{ database::repository::{DBRepository, GenericDBRepositoryConfig}, + repository::Visibility, user::permissions::{HasPermissions, RepositoryActions}, }; use serde::Deserialize; use serde_json::Value; -use tracing::{error, info, instrument}; +use tracing::{debug, error, info, instrument}; use utoipa::ToSchema; use uuid::Uuid; use crate::{ app::{ - authentication::Authentication, + authentication::{Authentication, AuthenticationError}, responses::{ no_content_response_with_error, InvalidRepositoryConfig, MissingPermission, RepositoryNotFound, ResponseBuilderExt, @@ -27,7 +28,7 @@ use crate::{ NitroRepo, }, error::InternalError, - repository::Repository, + repository::{self, Repository}, }; pub fn management_routes() -> Router { Router::new() @@ -168,39 +169,50 @@ pub struct GetConfigParams { #[instrument] pub async fn get_config( State(site): State, - auth: Authentication, + auth: Option, Query(params): Query, Path((repository, config)): Path<(Uuid, String)>, ) -> Result { - if !auth + let repository_visibility = Visibility::Private; + let Some(config_type) = site.get_repository_config_type(&config) else { + return Ok(InvalidRepositoryConfig::InvalidConfigType(config).into_response()); + }; + let config = + match GenericDBRepositoryConfig::get_config(repository, &config, site.as_ref()).await? { + Some(config) => config.value.0, + None => { + if params.default { + debug!("Getting default config for config type: {}", config); + config_type.default()? + } else { + return Ok(RepositoryNotFound::Uuid(repository).into_response()); + } + } + }; + let config = if auth .has_action(RepositoryActions::Edit, repository, &site.database) .await? { - return Ok(MissingPermission::EditRepository(repository).into_response()); - } - let config = match GenericDBRepositoryConfig::get_config(repository, &config, site.as_ref()) - .await? - { - Some(config) => config.value.0, - None => { - if params.default { - let Some(config_type) = site.get_repository_config_type(&config) else { - return Ok(InvalidRepositoryConfig::InvalidConfigType(config).into_response()); - }; - match config_type.default() { - Ok(ok) => ok, - Err(_) => { - return Ok(RepositoryNotFound::Uuid(repository).into_response()); - } - } - } else { - return Ok(RepositoryNotFound::Uuid(repository).into_response()); + Some(config) + } else { + // User does not have permission to view the config. Sanitize it + // If None is returned, the user does not have permission to view the config + debug!("Sanitizing config for public view"); + match repository_visibility { + Visibility::Hidden | Visibility::Public => { + config_type.sanitize_for_public_view(config)? } + _ => None, } }; - Response::builder() - .status(StatusCode::OK) - .json_body(&config) + if let Some(config) = config { + Ok(Response::builder() + .status(StatusCode::OK) + .json_body(&config)?) + } else { + + Ok(AuthenticationError::Forbidden.into_response()) + } } /// Updates a config for a repository /// diff --git a/nitro_repo/src/app/authentication/mod.rs b/nitro_repo/src/app/authentication/mod.rs index 896d32ce..fdbcaad0 100644 --- a/nitro_repo/src/app/authentication/mod.rs +++ b/nitro_repo/src/app/authentication/mod.rs @@ -39,6 +39,8 @@ pub enum AuthenticationError { PasswordVerificationError, #[error("No Auth Token Allowed here")] AuthTokenForbidden, + #[error("Forbidden")] + Forbidden, } impl From for AuthenticationError { fn from(err: sqlx::Error) -> Self { diff --git a/nitro_repo/src/app/authentication/session.rs b/nitro_repo/src/app/authentication/session.rs index 7f475b87..b25a16e9 100644 --- a/nitro_repo/src/app/authentication/session.rs +++ b/nitro_repo/src/app/authentication/session.rs @@ -7,7 +7,10 @@ use std::{ }, }; -use crate::app::config::{get_current_directory, Mode}; +use crate::{ + app::config::{get_current_directory, Mode}, + error::IntoErrorResponse, +}; use axum::response::{IntoResponse, Response}; use chrono::{DateTime, Duration, FixedOffset, Local}; use http::StatusCode; @@ -45,6 +48,11 @@ impl IntoResponse for SessionError { .unwrap() } } +impl IntoErrorResponse for SessionError { + fn into_response_boxed(self: Box) -> axum::response::Response { + (*self).into_response() + } +} #[derive(Debug, Deserialize, Serialize, Clone)] pub struct SessionManagerConfig { #[serde(with = "nr_core::utils::duration_serde::as_seconds")] diff --git a/nitro_repo/src/app/mod.rs b/nitro_repo/src/app/mod.rs index cac60679..15445689 100644 --- a/nitro_repo/src/app/mod.rs +++ b/nitro_repo/src/app/mod.rs @@ -124,10 +124,7 @@ impl NitroRepo { let database = PgPool::connect_with(database.into()) .await .context("Could not connec to database")?; - sqlx::migrate!() - .run(&database) - .await - .context("Failed to run Migrations")?; + nr_core::database::migration::run_migrations(&database).await?; Ok(database) } pub async fn new( diff --git a/nitro_repo/src/error/internal_error.rs b/nitro_repo/src/error/internal_error.rs deleted file mode 100644 index 520753be..00000000 --- a/nitro_repo/src/error/internal_error.rs +++ /dev/null @@ -1,47 +0,0 @@ -use axum::response::{IntoResponse, Response}; -use http::StatusCode; -use thiserror::Error; -use tracing::error; - -use crate::app::authentication::session::SessionError; - -/// Errors that happen internally to the system. -/// Not as a direct result of a Request -#[derive(Error, Debug)] -pub enum InternalError { - #[error("Json Parsing error {0}")] - JSONError(#[from] serde_json::Error), - #[error("Internal IO error {0}")] - IOError(#[from] std::io::Error), - #[error("Database error {0}")] - DBError(#[from] sqlx::Error), - #[error("Password Hash Error: {0}")] - PasswordHashError(#[from] argon2::password_hash::Error), - #[error("Argon2 Error: {0}")] - Argon2Error(#[from] argon2::Error), - #[error("Session Error {0}")] - SessionError(#[from] SessionError), - #[error("Storage Error {0}")] - StorageError(#[from] nr_storage::StorageError), - #[error("Unable to build HTTP Response {0}")] - ResponseError(#[from] http::Error), -} - -impl IntoResponse for InternalError { - fn into_response(self) -> Response { - match self { - Self::SessionError(err) => err.into_response(), - other => { - error!("{}", other); - let message = format!( - "Internal Service Error. Please Contact the System Admin. Error: {}", - other - ); - Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(message.into()) - .unwrap() - } - } - } -} diff --git a/nitro_repo/src/error/mod.rs b/nitro_repo/src/error/mod.rs index 16323643..5dd3ad59 100644 --- a/nitro_repo/src/error/mod.rs +++ b/nitro_repo/src/error/mod.rs @@ -1,11 +1,29 @@ mod bad_requests; -mod internal_error; -use std::error::Error; +use std::{error::Error, fmt::Display, io}; -use axum::response::IntoResponse; +use axum::{body::Body, response::IntoResponse}; pub use bad_requests::*; -use derive_more::derive::From; -pub use internal_error::*; +use nr_core::repository::config::{RepositoryConfigError, RepositoryConfigType}; +//pub use internal_error::*; +use nr_storage::StorageError; + +/// Allows creating a response from an error +pub trait IntoErrorResponse: Error + Send + Sync { + /// Converts the error into a response + /// + /// It must be of type of Box to allow for dynamic dispatch + fn into_response_boxed(self: Box) -> axum::response::Response; +} +macro_rules! impl_into_error_response_for_axum_into_response { + ($t:ty) => { + impl IntoErrorResponse for $t { + fn into_response_boxed(self: Box) -> axum::response::Response { + ::into_response(*self) + } + } + }; +} + #[derive(Debug, thiserror::Error)] #[error("Illegal State: {0}")] pub struct IllegalStateError(pub &'static str); @@ -17,40 +35,104 @@ impl IntoResponse for IllegalStateError { .unwrap() } } -#[derive(Debug, From)] -pub struct SQLXError(pub sqlx::Error); -impl IntoResponse for SQLXError { - fn into_response(self) -> axum::response::Response { +impl_into_error_response_for_axum_into_response!(IllegalStateError); + +fn internal_error_message(err: impl Error) -> Body { + format!( + "Internal Service Error. Please Contact the System Admin. Error: {}", + err + ) + .into() +} +impl IntoErrorResponse for reqwest::Error { + fn into_response_boxed(self: Box) -> axum::response::Response { axum::response::Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body( - format!( - "Internal Service Error. Please Contact the System Admin. Error: {}", - self.0 - ) - .into(), - ) + .body(internal_error_message(self)) .unwrap() } } -/// Allows creating a response from an error -pub trait IntoErrorResponse: Error + Send + Sync { - /// Converts the error into a response - /// - /// It must be of type of Box to allow for dynamic dispatch - fn into_response_boxed(self: Box) -> axum::response::Response; +impl IntoErrorResponse for io::Error { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) + .unwrap() + } +} +impl IntoErrorResponse for StorageError { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) + .unwrap() + } } impl IntoErrorResponse for sqlx::Error { fn into_response_boxed(self: Box) -> axum::response::Response { axum::response::Response::builder() .status(http::StatusCode::INTERNAL_SERVER_ERROR) - .body( - format!( - "Internal Service Error. Please Contact the System Admin. Error: {}", - self - ) - .into(), - ) + .body(internal_error_message(self)) + .unwrap() + } +} +impl IntoErrorResponse for serde_json::Error { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) + .unwrap() + } +} +impl IntoErrorResponse for http::Error { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) + .unwrap() + } +} +impl IntoErrorResponse for argon2::Error { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) .unwrap() } } +impl IntoErrorResponse for argon2::password_hash::Error { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) + .unwrap() + } +} +impl IntoErrorResponse for RepositoryConfigError { + fn into_response_boxed(self: Box) -> axum::response::Response { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body(internal_error_message(self)) + .unwrap() + } +} +#[derive(Debug)] +pub struct InternalError(pub Box); +impl Display for InternalError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl Error for InternalError {} + +impl IntoResponse for InternalError { + fn into_response(self) -> axum::response::Response { + self.0.into_response_boxed() + } +} + +impl From for InternalError { + fn from(err: T) -> Self { + InternalError(Box::new(err)) + } +} diff --git a/nitro_repo/src/repository/error.rs b/nitro_repo/src/repository/error.rs new file mode 100644 index 00000000..bb69312a --- /dev/null +++ b/nitro_repo/src/repository/error.rs @@ -0,0 +1,85 @@ +use std::fmt::Display; + +use axum::{body::Body, response::IntoResponse, response::Response}; +use http::StatusCode; +use thiserror::Error; + +use crate::{ + app::authentication::AuthenticationError, + error::{BadRequestErrors, IntoErrorResponse}, +}; + +#[derive(Debug, Error)] +pub enum RepositoryHandlerError { + #[error("Database Error: {0}")] + SQLXError(#[from] sqlx::Error), + #[error("Storage Error: {0}")] + StorageError(#[from] nr_storage::StorageError), + #[error("Unexpected Missing Body")] + MissingBody, + #[error("Invalid JSON: {0}")] + InvalidJson(#[from] serde_json::Error), + #[error("IO Error: {0}")] + IOError(#[from] std::io::Error), + #[error("Authentication Error: {0}")] + AuthenticationError(#[from] AuthenticationError), + #[error("{0}")] + Other(Box), +} + +impl From for RepositoryHandlerError { + fn from(error: BadRequestErrors) -> Self { + RepositoryHandlerError::Other(Box::new(error)) + } +} + +impl IntoResponse for RepositoryHandlerError { + fn into_response(self) -> Response { + match self { + RepositoryHandlerError::StorageError(error) => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from(format!( + "Error from Internal Storage System. Please contact your admin \n {}", + error + ))) + .unwrap(), + RepositoryHandlerError::Other(error) => error.into_response_boxed(), + other => Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from(format!( + "Internal Service Error Please contact your admin \n {}", + other + ))) + .unwrap(), + } + } +} +impl IntoErrorResponse for RepositoryHandlerError { + fn into_response_boxed(self: Box) -> Response { + self.into_response() + } +} + +/// A DynRepositoryHandlerError is a boxed version of a IntoErrorResponse +/// +/// impl From for DynRepositoryHandlerError is required because we can't impl IntoBoxedResponse for DynRepositoryHandlerError or +/// it will create conflicting implementations +#[derive(Debug)] +pub struct DynRepositoryHandlerError(pub Box); +impl Display for DynRepositoryHandlerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} +impl std::error::Error for DynRepositoryHandlerError {} +impl IntoResponse for DynRepositoryHandlerError { + fn into_response(self) -> axum::response::Response { + self.0.into_response_boxed() + } +} + +impl IntoErrorResponse for DynRepositoryHandlerError { + fn into_response_boxed(self: Box) -> axum::response::Response { + self.into_response() + } +} diff --git a/nitro_repo/src/repository/maven/hosted.rs b/nitro_repo/src/repository/maven/hosted.rs index d25ef461..8e78c84e 100644 --- a/nitro_repo/src/repository/maven/hosted.rs +++ b/nitro_repo/src/repository/maven/hosted.rs @@ -33,11 +33,14 @@ use crate::{ repository::{ maven::{configs::MavenPushRulesConfigType, MavenRepositoryConfigType}, utils::RepositoryExt, - Repository, RepositoryFactoryError, RepositoryHandlerError, + Repository, RepositoryFactoryError, }, }; -use super::{configs::MavenPushRules, utils::MavenRepositoryExt, RepoResponse, RepositoryRequest}; +use super::{ + configs::MavenPushRules, utils::MavenRepositoryExt, MavenError, RepoResponse, + RepositoryRequest, REPOSITORY_TYPE_ID, +}; #[derive(derive_more::Debug)] pub struct MavenHostedInner { pub id: Uuid, @@ -66,7 +69,7 @@ impl MavenHosted { path, authentication, }: RepositoryRequest, - ) -> Result { + ) -> Result { let user_id = if let Some(user) = authentication.get_user() { user.id } else { @@ -132,6 +135,7 @@ impl MavenHosted { } } impl Repository for MavenHosted { + type Error = MavenError; #[inline(always)] fn site(&self) -> NitroRepo { self.0.site.clone() @@ -146,7 +150,7 @@ impl Repository for MavenHosted { } #[inline(always)] fn get_type(&self) -> &'static str { - "maven" + &REPOSITORY_TYPE_ID } #[inline(always)] fn name(&self) -> String { @@ -213,7 +217,7 @@ impl Repository for MavenHosted { authentication, .. }: RepositoryRequest, - ) -> Result { + ) -> Result { if let Some(err) = self.check_read(&authentication).await? { return Ok(err); } @@ -230,7 +234,7 @@ impl Repository for MavenHosted { authentication, .. }: RepositoryRequest, - ) -> Result { + ) -> Result { let visibility = self.visibility(); if let Some(err) = self.check_read(&authentication).await? { return Ok(err); @@ -239,10 +243,7 @@ impl Repository for MavenHosted { return self.indexing_check_option(file, &authentication).await; } #[instrument(name = "maven_hosted_put")] - async fn handle_put( - &self, - request: RepositoryRequest, - ) -> Result { + async fn handle_put(&self, request: RepositoryRequest) -> Result { info!("Handling PUT Request for Repository: {}", self.id); { let push_rules = self.push_rules.read(); @@ -278,10 +279,7 @@ impl Repository for MavenHosted { self.get_type(), )) } - async fn handle_post( - &self, - request: RepositoryRequest, - ) -> Result { + async fn handle_post(&self, request: RepositoryRequest) -> Result { let Some(nitro_deploy_version) = request.get_nitro_repo_deploy_header()? else { return Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -296,7 +294,7 @@ impl Repository for MavenHosted { async fn resolve_project_and_version_for_path( &self, path: StoragePath, - ) -> Result { + ) -> Result { let path_as_string = path.to_string(); let version = DBProjectVersion::find_by_version_directory( &path_as_string, diff --git a/nitro_repo/src/repository/maven/mod.rs b/nitro_repo/src/repository/maven/mod.rs index c67a4f3a..bcb58e49 100644 --- a/nitro_repo/src/repository/maven/mod.rs +++ b/nitro_repo/src/repository/maven/mod.rs @@ -24,13 +24,13 @@ pub mod hosted; pub mod nitro_deploy; pub mod proxy; pub mod utils; - +pub static REPOSITORY_TYPE_ID: &str = "maven"; #[derive(Debug, Default)] pub struct MavenRepositoryType; impl RepositoryType for MavenRepositoryType { fn get_type(&self) -> &'static str { - "maven" + REPOSITORY_TYPE_ID } fn config_types(&self) -> Vec<&str> { @@ -101,6 +101,7 @@ impl RepositoryType for MavenRepositoryType { } } #[derive(Debug, Clone, DynRepositoryHandler)] +#[repository_handler(error=MavenError)] pub enum MavenRepository { Hosted(MavenHosted), Proxy(MavenProxy), @@ -139,21 +140,37 @@ impl MavenRepository { pub enum MavenError { #[error("Error with processing Maven request: {0}")] MavenRS(#[from] maven_rs::Error), - #[error("Storage Error")] - Storage(#[from] nr_storage::StorageError), #[error("XML Deserialize Error: {0}")] XMLDeserialize(#[from] maven_rs::quick_xml::DeError), - #[error("Database Error: {0}")] - Database(#[from] sqlx::Error), #[error("Internal Error. {0}")] BuilderError(#[from] builder_error::BuilderError), #[error("Missing From Pom: {0}")] MissingFromPom(&'static str), - #[error("Failed to proxy request {0}")] - ReqwestError(#[from] reqwest::Error), - #[error(transparent)] - BadRequest(#[from] BadRequestErrors), + #[error("{0}")] + Other(Box), +} +impl From for DynRepositoryHandlerError { + fn from(err: MavenError) -> Self { + DynRepositoryHandlerError(Box::new(err)) + } +} +macro_rules! impl_from_error_for_other { + ($t:ty) => { + impl From<$t> for MavenError { + fn from(e: $t) -> Self { + MavenError::Other(Box::new(e)) + } + } + }; } +impl_from_error_for_other!(BadRequestErrors); +impl_from_error_for_other!(sqlx::Error); +impl_from_error_for_other!(serde_json::Error); +impl_from_error_for_other!(std::io::Error); +impl_from_error_for_other!(AuthenticationError); +impl_from_error_for_other!(RepositoryHandlerError); +impl_from_error_for_other!(nr_storage::StorageError); +impl_from_error_for_other!(reqwest::Error); impl IntoErrorResponse for MavenError { fn into_response_boxed(self: Box) -> axum::response::Response { @@ -181,7 +198,6 @@ impl IntoResponse for MavenError { .status(500) .body(axum::body::Body::from(format!("Maven Error: {}", e))) .unwrap(), - MavenError::BadRequest(e) => e.into_response(), err => axum::http::Response::builder() .status(500) .body(axum::body::Body::from(format!( diff --git a/nitro_repo/src/repository/maven/proxy.rs b/nitro_repo/src/repository/maven/proxy.rs index f19b2832..3fab8483 100644 --- a/nitro_repo/src/repository/maven/proxy.rs +++ b/nitro_repo/src/repository/maven/proxy.rs @@ -32,8 +32,8 @@ use crate::{app::NitroRepo, repository::Repository}; use super::{ repo_type::RepositoryFactoryError, utils::MavenRepositoryExt, MavenError, - MavenRepositoryConfig, MavenRepositoryConfigType, RepoResponse, RepositoryHandlerError, - RepositoryRequest, + MavenRepositoryConfig, MavenRepositoryConfigType, RepoResponse, RepositoryRequest, + REPOSITORY_TYPE_ID, }; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct MavenProxyConfig { @@ -219,6 +219,7 @@ impl MavenProxy { } impl Repository for MavenProxy { + type Error = MavenError; fn get_storage(&self) -> nr_storage::DynStorage { self.0.storage.clone() } @@ -227,7 +228,7 @@ impl Repository for MavenProxy { } fn get_type(&self) -> &'static str { - "maven" + &REPOSITORY_TYPE_ID } fn config_types(&self) -> Vec<&str> { @@ -296,7 +297,7 @@ impl Repository for MavenProxy { authentication, .. }: RepositoryRequest, - ) -> Result { + ) -> Result { if let Some(err) = self.check_read(&authentication).await? { return Ok(err); } @@ -326,7 +327,7 @@ impl Repository for MavenProxy { authentication, .. }: RepositoryRequest, - ) -> Result { + ) -> Result { let visibility = self.visibility(); // TODO: Proxy HEAD request if let Some(err) = self.check_read(&authentication).await? { diff --git a/nitro_repo/src/repository/maven/utils.rs b/nitro_repo/src/repository/maven/utils.rs index 5ff6bd07..ab020733 100644 --- a/nitro_repo/src/repository/maven/utils.rs +++ b/nitro_repo/src/repository/maven/utils.rs @@ -44,7 +44,7 @@ pub trait MavenRepositoryExt: Repository + Debug { &self, file_response: T, authentication: &RepositoryAuthentication, - ) -> Result + ) -> Result where RepoResponse: From, { @@ -65,7 +65,7 @@ pub trait MavenRepositoryExt: Repository + Debug { &self, file_response: Option, authentication: &RepositoryAuthentication, - ) -> Result + ) -> Result where RepoResponse: From, RepoResponse: From>, diff --git a/nitro_repo/src/repository/mod.rs b/nitro_repo/src/repository/mod.rs index 3851adbf..2973f2f9 100644 --- a/nitro_repo/src/repository/mod.rs +++ b/nitro_repo/src/repository/mod.rs @@ -2,19 +2,14 @@ use std::{fmt::Debug, future::Future}; -use axum::{ - body::Body, - response::{IntoResponse, Response}, -}; - -use ::http::StatusCode; - use nr_core::{ repository::{project::ProjectResolution, Visibility}, storage::StoragePath, }; + pub mod prelude { - pub use super::{RepoResponse, Repository, RepositoryHandlerError, RepositoryRequest}; + pub use super::{DynRepositoryHandlerError, RepositoryFactoryError, RepositoryHandlerError}; + pub use super::{RepoResponse, Repository, RepositoryRequest}; pub use crate::app::NitroRepo; pub use axum::response::{IntoResponse, Response}; pub use http::StatusCode; @@ -30,14 +25,16 @@ pub mod maven; pub mod npm; mod repo_type; pub use repo_type::*; -use thiserror::Error; use uuid::Uuid; +mod error; pub mod utils; use crate::{ app::{authentication::AuthenticationError, NitroRepo}, error::{BadRequestErrors, IntoErrorResponse}, }; +pub use error::*; pub trait Repository: Send + Sync + Clone + Debug { + type Error: IntoErrorResponse + 'static; fn get_storage(&self) -> DynStorage; /// The Repository type. This is used to identify the Repository type in the database fn get_type(&self) -> &'static str; @@ -52,7 +49,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn resolve_project_and_version_for_path( &self, path: StoragePath, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(ProjectResolution { project: None, @@ -68,7 +65,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_get( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -80,7 +77,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_post( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -92,7 +89,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_put( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -104,7 +101,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_patch( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -115,7 +112,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_delete( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -127,7 +124,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_head( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -138,7 +135,7 @@ pub trait Repository: Send + Sync + Clone + Debug { fn handle_other( &self, request: RepositoryRequest, - ) -> impl Future> + Send { + ) -> impl Future> + Send { async { Ok(RepoResponse::unsupported_method_response( request.parts.method, @@ -147,54 +144,9 @@ pub trait Repository: Send + Sync + Clone + Debug { } } } - #[derive(Debug, Clone, DynRepositoryHandler)] +#[repository_handler(error = DynRepositoryHandlerError)] pub enum DynRepository { Maven(maven::MavenRepository), NPM(npm::NPMRegistry), } -#[derive(Debug, Error)] -pub enum RepositoryHandlerError { - #[error("Database Error: {0}")] - SQLXError(#[from] sqlx::Error), - #[error("Storage Error: {0}")] - StorageError(#[from] nr_storage::StorageError), - #[error("Unexpected Missing Body")] - MissingBody, - #[error("Invalid JSON: {0}")] - InvalidJson(#[from] serde_json::Error), - #[error("IO Error: {0}")] - IOError(#[from] std::io::Error), - #[error("Authentication Error: {0}")] - AuthenticationError(#[from] AuthenticationError), - #[error("{0}")] - Other(Box), -} - -impl From for RepositoryHandlerError { - fn from(error: BadRequestErrors) -> Self { - RepositoryHandlerError::Other(Box::new(error)) - } -} - -impl IntoResponse for RepositoryHandlerError { - fn into_response(self) -> Response { - match self { - RepositoryHandlerError::StorageError(error) => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(format!( - "Error from Internal Storage System. Please contact your admin \n {}", - error - ))) - .unwrap(), - RepositoryHandlerError::Other(error) => error.into_response_boxed(), - other => Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::from(format!( - "Internal Service Error Please contact your admin \n {}", - other - ))) - .unwrap(), - } - } -} diff --git a/nitro_repo/src/repository/npm/hosted.rs b/nitro_repo/src/repository/npm/hosted.rs index ad74168d..e0e89d61 100644 --- a/nitro_repo/src/repository/npm/hosted.rs +++ b/nitro_repo/src/repository/npm/hosted.rs @@ -8,8 +8,7 @@ use crate::{ repository::{ npm::{types::PublishRequest, NPMRegistryConfigType, NPMRegistryError}, utils::RepositoryExt, - RepoResponse, Repository, RepositoryFactoryError, RepositoryHandlerError, - RepositoryRequest, + RepoResponse, Repository, RepositoryFactoryError, RepositoryRequest, }, }; use ahash::{HashMap, HashMapExt}; @@ -53,7 +52,7 @@ impl NPMHostedRegistry { async fn handle_publish( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { let Some(user) = request .authentication .get_user_if_has_action(RepositoryActions::Write, self.id, self.site.as_ref()) @@ -111,6 +110,7 @@ impl NPMHostedRegistry { impl NpmRegistryExt for NPMHostedRegistry {} impl RepositoryExt for NPMHostedRegistry {} impl Repository for NPMHostedRegistry { + type Error = NPMRegistryError; fn get_storage(&self) -> DynStorage { self.0.storage.clone() } @@ -145,7 +145,7 @@ impl Repository for NPMHostedRegistry { async fn handle_get( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { let headers = request.headers(); let path_as_string = request.path.to_string(); debug!(?headers, ?path_as_string, "Handling NPM GET request"); @@ -281,7 +281,7 @@ impl Repository for NPMHostedRegistry { async fn handle_put( &self, request: RepositoryRequest, - ) -> Result { + ) -> Result { let path_as_string = request.path.to_string(); debug!( ?path_as_string, diff --git a/nitro_repo/src/repository/npm/login/couch_db.rs b/nitro_repo/src/repository/npm/login/couch_db.rs index 4396347d..60c2f4f5 100644 --- a/nitro_repo/src/repository/npm/login/couch_db.rs +++ b/nitro_repo/src/repository/npm/login/couch_db.rs @@ -11,7 +11,7 @@ use tracing::{debug, instrument}; use crate::{ app::authentication::verify_login, repository::{ - npm::{login::LoginResponse, utils::NpmRegistryExt}, + npm::{login::LoginResponse, utils::NpmRegistryExt, NPMRegistryError}, RepoResponse, RepositoryHandlerError, RepositoryRequest, }, }; @@ -50,7 +50,7 @@ pub struct CouchDBLoginResponse { pub async fn perform_login( repository: &impl NpmRegistryExt, request: RepositoryRequest, -) -> Result { +) -> Result { let path_as_string = request.path.to_string(); let Some(source) = request .user_agent_as_string()? diff --git a/nitro_repo/src/repository/npm/login/web_login.rs b/nitro_repo/src/repository/npm/login/web_login.rs index f8fce12d..654f5a67 100644 --- a/nitro_repo/src/repository/npm/login/web_login.rs +++ b/nitro_repo/src/repository/npm/login/web_login.rs @@ -1,7 +1,8 @@ use serde::{Deserialize, Serialize}; use crate::repository::{ - npm::utils::NpmRegistryExt, RepoResponse, RepositoryHandlerError, RepositoryRequest, + npm::{utils::NpmRegistryExt, NPMRegistryError}, + RepoResponse, RepositoryHandlerError, RepositoryRequest, }; use super::LoginResponse; @@ -13,7 +14,7 @@ pub struct WebLoginResponse { pub async fn perform_login( repository: &impl NpmRegistryExt, request: RepositoryRequest, -) -> Result { +) -> Result { // TODO: Implement Web Login return Ok(LoginResponse::UnsupportedLogin.into()); } diff --git a/nitro_repo/src/repository/npm/mod.rs b/nitro_repo/src/repository/npm/mod.rs index a1ab76d2..3c6ccd22 100644 --- a/nitro_repo/src/repository/npm/mod.rs +++ b/nitro_repo/src/repository/npm/mod.rs @@ -20,7 +20,10 @@ pub mod hosted; pub mod login; pub mod types; pub mod utils; -use crate::error::{IntoErrorResponse, SQLXError}; +use crate::{ + app::authentication::AuthenticationError, + error::{BadRequestErrors, IntoErrorResponse}, +}; pub use super::prelude::*; mod configs; @@ -30,14 +33,13 @@ use super::{ pub use configs::*; #[derive(Debug, Clone, DynRepositoryHandler)] +#[repository_handler(error=NPMRegistryError)] pub enum NPMRegistry { Hosted(hosted::NPMHostedRegistry), } #[derive(Debug, thiserror::Error)] pub enum NPMRegistryError { - #[error(transparent)] - DatabaseError(#[from] sqlx::Error), #[error(transparent)] InvalidName(#[from] InvalidNPMPackageName), #[error( @@ -55,27 +57,51 @@ pub enum NPMRegistryError { InvalidPackageAttachment(DecodeError), #[error("Only one release or attachment can be uploaded at a time")] OnlyOneReleaseOrAttachmentAtATime, + #[error("{0}")] + Other(Box), +} +impl From for RepositoryHandlerError { + fn from(err: NPMRegistryError) -> Self { + RepositoryHandlerError::Other(Box::new(err)) + } } +macro_rules! impl_from_error_for_other { + ($t:ty) => { + impl From<$t> for NPMRegistryError { + fn from(e: $t) -> Self { + NPMRegistryError::Other(Box::new(e)) + } + } + }; +} +impl_from_error_for_other!(BadRequestErrors); +impl_from_error_for_other!(sqlx::Error); +impl_from_error_for_other!(serde_json::Error); +impl_from_error_for_other!(std::io::Error); +impl_from_error_for_other!(AuthenticationError); +impl_from_error_for_other!(RepositoryHandlerError); +impl_from_error_for_other!(nr_storage::StorageError); + impl IntoErrorResponse for NPMRegistryError { fn into_response_boxed(self: Box) -> axum::response::Response { self.into_response() } } -impl From for RepositoryHandlerError { +impl From for DynRepositoryHandlerError { fn from(err: NPMRegistryError) -> Self { - RepositoryHandlerError::Other(Box::new(err)) + DynRepositoryHandlerError(Box::new(err)) } } impl IntoResponse for NPMRegistryError { fn into_response(self) -> Response { match self { - NPMRegistryError::DatabaseError(err) => SQLXError(err).into_response(), NPMRegistryError::InvalidGetRequest => Response::builder() .status(StatusCode::NOT_FOUND) .body("Invalid GET request".into()) .unwrap(), + NPMRegistryError::Other(other) => other.into_response_boxed(), bad_request => { debug!("Bad Request: {:?}", bad_request); Response::builder() diff --git a/nitro_repo/src/repository/repo_http.rs b/nitro_repo/src/repository/repo_http.rs index c97f00f9..24e84b6c 100644 --- a/nitro_repo/src/repository/repo_http.rs +++ b/nitro_repo/src/repository/repo_http.rs @@ -5,7 +5,7 @@ use crate::{ authentication::AuthenticationError, responses::RepositoryNotFound, NitroRepo, RepositoryStorageName, }, - error::{BadRequestErrors, IllegalStateError}, + error::{BadRequestErrors, IllegalStateError, IntoErrorResponse}, repository::Repository, utils::headers::date_time::date_time_for_header, }; @@ -26,12 +26,15 @@ use http_body_util::BodyExt; use nr_core::storage::{InvalidStoragePath, StoragePath}; use nr_storage::{StorageFile, StorageFileMeta, StorageFileReader}; use serde::Deserialize; +use thiserror::Error; use tracing::{debug, error, instrument, span, Level}; mod header; mod repo_auth; -use super::RepositoryHandlerError; pub use header::*; pub use repo_auth::*; + +use super::RepositoryHandlerError; + #[derive(Debug, From)] pub struct RepositoryRequestBody(Body); impl RepositoryRequestBody {