From 53e223a6c88bc38c1c3bfacb1cd231e4d9409fbe Mon Sep 17 00:00:00 2001 From: Wyatt Herkamp Date: Tue, 27 Aug 2024 20:21:56 -0400 Subject: [PATCH] NPM Support Work --- Cargo.lock | 100 +++--- crates/core/src/builder_error.rs | 16 + crates/core/src/database/mod.rs | 3 + crates/core/src/database/project/mod.rs | 11 + crates/core/src/database/project/new.rs | 10 +- crates/core/src/database/project/update.rs | 20 +- crates/core/src/lib.rs | 1 + crates/core/src/repository/project/mod.rs | 2 + crates/core/src/storage/storage_path.rs | 74 +++- crates/core/src/utils.rs | 19 + crates/core/src/utils/time.rs | 36 ++ docs/docs/repositoryTypes/npm/errors.md | 8 + docs/docs/repositoryTypes/npm/standard.md | 37 ++ nitro_repo/Cargo.toml | 3 +- nitro_repo/src/app/authentication/mod.rs | 6 + nitro_repo/src/app/mod.rs | 3 +- nitro_repo/src/error/bad_requests.rs | 11 + nitro_repo/src/error/mod.rs | 37 ++ nitro_repo/src/repository/maven/mod.rs | 30 +- nitro_repo/src/repository/maven/proxy.rs | 2 +- nitro_repo/src/repository/maven/utils.rs | 6 +- nitro_repo/src/repository/mod.rs | 31 +- nitro_repo/src/repository/npm/configs.rs | 48 +++ nitro_repo/src/repository/npm/hosted.rs | 317 +++++++++++++++++ .../src/repository/npm/login/couch_db.rs | 79 +++++ nitro_repo/src/repository/npm/login/mod.rs | 43 +++ .../src/repository/npm/login/web_login.rs | 12 + nitro_repo/src/repository/npm/mod.rs | 239 +++++++------ .../src/repository/npm/registry_type.rs | 70 ---- nitro_repo/src/repository/npm/types/login.rs | 93 ----- nitro_repo/src/repository/npm/types/mod.rs | 22 +- nitro_repo/src/repository/npm/types/name.rs | 162 +++++++++ .../src/repository/npm/types/publish.rs | 56 +++ .../src/repository/npm/types/request.rs | 325 +++++++++++++++++- nitro_repo/src/repository/npm/utils.rs | 75 ++++ nitro_repo/src/repository/repo_http.rs | 42 ++- nitro_repo/src/repository/utils.rs | 23 ++ site/package-lock.json | 207 ++++++----- site/package.json | 2 +- .../repository/types/npm/NPMConfig.vue | 98 ++++++ .../repository/types/npm/NPMProjectHelper.vue | 25 ++ .../components/repository/types/npm/npm.ts | 38 ++ site/src/types/repository.ts | 6 + 43 files changed, 1937 insertions(+), 511 deletions(-) create mode 100644 crates/core/src/builder_error.rs create mode 100644 crates/core/src/utils/time.rs create mode 100644 docs/docs/repositoryTypes/npm/errors.md create mode 100644 docs/docs/repositoryTypes/npm/standard.md create mode 100644 nitro_repo/src/repository/npm/configs.rs create mode 100644 nitro_repo/src/repository/npm/hosted.rs create mode 100644 nitro_repo/src/repository/npm/login/couch_db.rs create mode 100644 nitro_repo/src/repository/npm/login/mod.rs create mode 100644 nitro_repo/src/repository/npm/login/web_login.rs delete mode 100644 nitro_repo/src/repository/npm/registry_type.rs delete mode 100644 nitro_repo/src/repository/npm/types/login.rs create mode 100644 nitro_repo/src/repository/npm/types/name.rs create mode 100644 nitro_repo/src/repository/npm/types/publish.rs create mode 100644 nitro_repo/src/repository/npm/utils.rs create mode 100644 site/src/components/repository/types/npm/NPMConfig.vue create mode 100644 site/src/components/repository/types/npm/NPMProjectHelper.vue diff --git a/Cargo.lock b/Cargo.lock index f9bbec6d..f50e767c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,9 +565,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.14" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d2eb3cd3d1bf4529e31c215ee6f93ec5a3d536d9f578f93d9d33ee19562932" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "jobserver", "libc", @@ -726,9 +726,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "constant_time_eq" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cookie" @@ -955,6 +955,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -2034,6 +2040,7 @@ dependencies = [ "opentelemetry_sdk", "parking_lot", "pin-project", + "pretty_assertions", "rand", "redb", "regex", @@ -2505,11 +2512,21 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a909e6e8053fa1a5ad670f5816c7d93029ee1fa8898718490544a6b0d5d38b3e" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", "syn", @@ -2661,9 +2678,9 @@ dependencies = [ [[package]] name = "redb" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6dd20d3cdeb9c7d2366a0b16b93b35b75aec15309fbeb7ce477138c9f68c8c0" +checksum = "58323dc32ea52a8ae105ff94bc0460c5d906307533ba3401aa63db3cbe491fe5" dependencies = [ "libc", ] @@ -2906,9 +2923,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" dependencies = [ "bitflags 2.6.0", "errno", @@ -2951,9 +2968,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.6" +version = "0.102.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" dependencies = [ "aws-lc-rs", "ring", @@ -3457,15 +3474,15 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stacker" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a5daa25ea337c85ed954c0496e3bdd2c7308cc3b24cf7b50d04876654c579f" +checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" dependencies = [ "cc", "cfg-if", "libc", "psm", - "windows-sys 0.36.1", + "windows-sys 0.59.0", ] [[package]] @@ -3750,9 +3767,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401" +checksum = "c6f6ba989e4b2c58ae83d862d3a3e27690b6e3ae630d0deb59f3697f32aa88ad" dependencies = [ "async-stream", "async-trait", @@ -4333,19 +4350,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -4416,12 +4420,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4434,12 +4432,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4458,12 +4450,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4476,12 +4462,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4506,12 +4486,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4533,6 +4507,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/crates/core/src/builder_error.rs b/crates/core/src/builder_error.rs new file mode 100644 index 00000000..898c81f7 --- /dev/null +++ b/crates/core/src/builder_error.rs @@ -0,0 +1,16 @@ +use derive_builder::UninitializedFieldError; +use thiserror::Error; + +#[derive(Debug, Clone, Error)] +pub enum BuilderError { + #[error("Uninitialized Field: {0}.")] + UninitializedField(&'static str), + #[error("Invalid Field: {0}.")] + InvalidField(String), +} + +impl From for BuilderError { + fn from(err: UninitializedFieldError) -> Self { + BuilderError::UninitializedField(err.field_name()) + } +} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 825b3b5a..9e03e44c 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -1,3 +1,6 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + pub mod project; pub mod repository; pub mod storage; diff --git a/crates/core/src/database/project/mod.rs b/crates/core/src/database/project/mod.rs index f8cc670d..5aeb32ca 100644 --- a/crates/core/src/database/project/mod.rs +++ b/crates/core/src/database/project/mod.rs @@ -200,4 +200,15 @@ impl DBProjectVersion { .await?; Ok(version) } + pub async fn get_all_versions( + project_id: Uuid, + database: &PgPool, + ) -> Result, sqlx::Error> { + let versions = + sqlx::query_as::<_, Self>(r#"SELECT * FROM project_versions WHERE project_id = $1"#) + .bind(project_id) + .fetch_all(database) + .await?; + Ok(versions) + } } diff --git a/crates/core/src/database/project/new.rs b/crates/core/src/database/project/new.rs index e8e21718..8aa92be4 100644 --- a/crates/core/src/database/project/new.rs +++ b/crates/core/src/database/project/new.rs @@ -1,3 +1,5 @@ +use crate::builder_error::BuilderError; +use crate::repository::project::{ReleaseType, VersionData}; use derive_builder::Builder; use serde::{Deserialize, Serialize}; use sqlx::{types::Json, PgPool}; @@ -5,10 +7,10 @@ use tracing::info; use utoipa::ToSchema; use uuid::Uuid; -use crate::repository::project::{ReleaseType, VersionData}; - use super::DBProject; #[derive(Debug, Clone, PartialEq, Eq, Builder)] +#[builder(build_fn(error = "BuilderError"))] + pub struct NewProject { #[builder(default)] pub scope: Option, @@ -111,6 +113,8 @@ impl NewProjectMember { } #[derive(Debug, Clone, PartialEq, Eq, Builder)] +#[builder(build_fn(error = "BuilderError"))] + pub struct NewVersion { pub project_id: Uuid, /// The version of the project @@ -159,7 +163,7 @@ impl NewVersion { sqlx::query( r#" UPDATE projects - SET latest_release = $1 AND latest_pre_release = $1 + SET latest_release = $1, latest_pre_release = $1 WHERE id = $2 "#, ) diff --git a/crates/core/src/database/project/update.rs b/crates/core/src/database/project/update.rs index cd61b6f7..953b53b2 100644 --- a/crates/core/src/database/project/update.rs +++ b/crates/core/src/database/project/update.rs @@ -1,5 +1,3 @@ -use derive_builder::Builder; -use http::version; use sqlx::{types::Json, Execute, PgPool, QueryBuilder}; use tracing::{info, instrument, warn}; @@ -8,10 +6,8 @@ use crate::repository::project::{ReleaseType, VersionData}; #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct UpdateProjectVersion { pub release_type: Option, - //#[builder(default)] - //pub publisher: Option, - //#[builder(default)] - //pub version_page: Option, + pub publisher: Option>, + pub version_page: Option>, pub extra: Option, } @@ -24,14 +20,18 @@ impl UpdateProjectVersion { separated.push("release_type = "); separated.push_bind_unseparated(release_type); } - //if let Some(version_page) = self.version_page { - // separated.push("user_manager = "); - // separated.push_bind_unseparated(version_page); - //} if let Some(extra) = self.extra { separated.push("extra = "); separated.push_bind_unseparated(Json(extra)); } + if let Some(version_page) = self.version_page { + separated.push("user_manager = "); + separated.push_bind_unseparated(version_page); + } + if let Some(publisher) = self.publisher { + separated.push("publisher = "); + separated.push_bind_unseparated(publisher); + } query.push(" WHERE id = "); query.push_bind(version_id); let query: sqlx::query::Query = query.build(); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 78511a02..7d75095c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,5 +1,6 @@ pub mod user; pub type ConfigTimeStamp = chrono::DateTime; +pub mod builder_error; pub mod database; pub mod repository; pub mod storage; diff --git a/crates/core/src/repository/project/mod.rs b/crates/core/src/repository/project/mod.rs index b9d293df..c795d3d4 100644 --- a/crates/core/src/repository/project/mod.rs +++ b/crates/core/src/repository/project/mod.rs @@ -74,6 +74,8 @@ pub enum ProjectState { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ToSchema, Default, Builder)] #[serde(default)] +#[builder(build_fn(error = "crate::builder_error::BuilderError"))] + pub struct VersionData { #[builder(default)] pub documentation_url: Option, diff --git a/crates/core/src/storage/storage_path.rs b/crates/core/src/storage/storage_path.rs index 0003eb2e..bb166d2f 100644 --- a/crates/core/src/storage/storage_path.rs +++ b/crates/core/src/storage/storage_path.rs @@ -6,31 +6,87 @@ use thiserror::Error; use tracing::instrument; #[derive(Debug, Clone, Hash, PartialEq, Eq)] -struct StoragePathComponent(String); +pub struct StoragePathComponent(String); +impl PartialEq<&str> for StoragePathComponent { + fn eq(&self, other: &&str) -> bool { + self.0 == *other + } +} +impl PartialEq for StoragePathComponent { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} +impl TryFrom<&str> for StoragePathComponent { + type Error = InvalidStoragePath; + #[instrument] + fn try_from(value: &str) -> Result { + if value.is_empty() { + return Err(InvalidStoragePath::InvalidPath); + } + if value.contains('/') { + return Err(InvalidStoragePath::InvalidPath); + } + Ok(StoragePathComponent(value.to_string())) + } +} +impl From for String { + fn from(value: StoragePathComponent) -> Self { + value.0 + } +} +impl Display for StoragePathComponent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} +impl AsRef for StoragePathComponent { + fn as_ref(&self) -> &str { + &self.0 + } +} /// A Storage path is a UTF-8 only path. Where the root is the base of the storage. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct StoragePath(Vec); + +impl Default for StoragePath { + fn default() -> Self { + StoragePath(vec![]) + } +} impl StoragePath { pub fn parent(self) -> Self { let mut path = self.0; path.pop(); StoragePath(path) } -} -impl Default for StoragePath { - fn default() -> Self { - StoragePath(vec![]) + pub fn number_of_components(&self) -> usize { + self.0.len() } -} -impl StoragePath { pub fn has_extension(&self, extension: &str) -> bool { self.0 .last() .map(|v| v.0.ends_with(extension)) .unwrap_or(false) } - pub fn push(&mut self, component: &str) { - self.0.push(StoragePathComponent(component.to_string())); + pub fn push(mut self, component: &str) -> Self { + let components = StoragePath::from(component); + self.0.extend(components.0); + self + } + pub fn push_mut(&mut self, component: &str) { + let components = StoragePath::from(component); + self.0.extend(components.0); + } +} +impl From> for StoragePath { + fn from(value: Vec) -> Self { + StoragePath(value) + } +} +impl From for Vec { + fn from(value: StoragePath) -> Self { + value.0 } } impl From for PathBuf { diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 0cd81cda..5bdfde04 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,3 +1,4 @@ +pub mod time; pub mod base64_utils { use base64::{engine::general_purpose::STANDARD, DecodeError, Engine}; use tracing::instrument; @@ -15,6 +16,24 @@ pub mod base64_utils { pub fn encode_basic_header(username: impl AsRef, password: impl AsRef) -> String { STANDARD.encode(format!("{}:{}", username.as_ref(), password.as_ref())) } + pub mod serde_base64 { + use serde::{Deserialize, Serialize}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + super::decode(string).map_err(serde::de::Error::custom) + } + pub fn serialize(data: &Vec, serializer: S) -> Result + where + S: serde::Serializer, + { + super::encode(data).serialize(serializer) + } + + } } pub mod sha256 { use sha2::Digest; diff --git a/crates/core/src/utils/time.rs b/crates/core/src/utils/time.rs new file mode 100644 index 00000000..08fd88e4 --- /dev/null +++ b/crates/core/src/utils/time.rs @@ -0,0 +1,36 @@ +pub mod iso_8601 { + use chrono::{DateTime, FixedOffset}; + use serde::{Deserialize, Serialize}; + + pub static ISO_8601: &str = "%Y-%m-%dT%H:%M:%S.%f"; + pub fn serialize(time: &DateTime, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + to_string(time).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + DateTime::parse_from_str(&s, ISO_8601).map_err(serde::de::Error::custom) + } + pub fn to_string(time: &DateTime) -> String { + time.format(ISO_8601).to_string() + } + pub fn from_string(s: &str) -> Result, chrono::ParseError> { + DateTime::::parse_from_rfc3339(s) + } + + #[cfg(test)] + mod tests { + #[test] + pub fn test() { + let from = super::from_string("2024-08-28T00:09:11.230Z").unwrap(); + println!("{:?}", from); + } + } +} diff --git a/docs/docs/repositoryTypes/npm/errors.md b/docs/docs/repositoryTypes/npm/errors.md new file mode 100644 index 00000000..68aadf7a --- /dev/null +++ b/docs/docs/repositoryTypes/npm/errors.md @@ -0,0 +1,8 @@ +# NPM Registry Errors + +## Invalid Package Name +NPM Packages must be lowercase, and can only contain letters, numbers, underscores, and dashes. If you try to publish a package with an invalid name, you will receive an error. + +### Invalid Tarball URL + +If you set your registry url to `{BASE_URL}/repositories/{storage}/{repsotiroy}`. This causes NPM to ignore the last part of the url. You need to add a trailing slash to the end of the url. \ No newline at end of file diff --git a/docs/docs/repositoryTypes/npm/standard.md b/docs/docs/repositoryTypes/npm/standard.md new file mode 100644 index 00000000..1b380907 --- /dev/null +++ b/docs/docs/repositoryTypes/npm/standard.md @@ -0,0 +1,37 @@ +## Publishing + +## Logging in + +When you run `npm adduser` it will first send a request to `{registry_url}/-/v1/login` +if this path resolves it should return a web login response. However, this feature is not documented here or implemented in Nitro Repo so this returns a bad request. + +So if that requests returns a bad request. It will attempt to do a couch db login. + + +### Couch DB Login + +Which is at route PUT `-/user/org.couchdb.user:{SOME_USER}` + +SOME_USER being the username. + +The body will be `application/json` with +```json +{ + "name": "Username", + "password": "Password", + "email": "Email", + "login_type": "user", + "roles": [], + "date": "CURRENT_ISO_8601_TIMESTAMP" +} +``` + +You will now validate the login and return a token if successful. + +Response is `application/json` with +```json +{ + "token": "TOKEN" +} +``` +NPM will save this to the config file and you will be logged in. \ No newline at end of file diff --git a/nitro_repo/Cargo.toml b/nitro_repo/Cargo.toml index 7f17e3ab..00ed485b 100644 --- a/nitro_repo/Cargo.toml +++ b/nitro_repo/Cargo.toml @@ -6,7 +6,8 @@ edition.workspace = true build = "build.rs" license.workspace = true - +[dev-dependencies] +pretty_assertions = "1.1" [dependencies] # Web axum = { version = "0.7", features = ["macros", "tokio"] } diff --git a/nitro_repo/src/app/authentication/mod.rs b/nitro_repo/src/app/authentication/mod.rs index 0da712cc..d52cb597 100644 --- a/nitro_repo/src/app/authentication/mod.rs +++ b/nitro_repo/src/app/authentication/mod.rs @@ -20,6 +20,7 @@ use thiserror::Error; use tracing::{error, instrument, warn}; use utoipa::ToSchema; +use crate::error::IntoErrorResponse; use crate::utils::headers::AuthorizationHeader; use super::NitroRepo; @@ -36,6 +37,11 @@ pub enum AuthenticationError { #[error("Password is not able to be verified.")] PasswordVerificationError, } +impl IntoErrorResponse for AuthenticationError { + fn into_response_boxed(self: Box) -> axum::response::Response { + self.into_response() + } +} impl IntoResponse for AuthenticationError { fn into_response(self) -> axum::response::Response { error!("{}", self); diff --git a/nitro_repo/src/app/mod.rs b/nitro_repo/src/app/mod.rs index 335486ad..cec6b90f 100644 --- a/nitro_repo/src/app/mod.rs +++ b/nitro_repo/src/app/mod.rs @@ -37,7 +37,7 @@ use uuid::Uuid; pub mod open_api; use crate::repository::{ maven::{MavenRepositoryConfigType, MavenRepositoryType}, - npm::registry_type::NpmRegistryType, + npm::{NPMRegistryConfigType, NpmRegistryType}, DynRepository, RepositoryType, StagingConfig, }; pub mod api; @@ -349,6 +349,7 @@ pub static REPOSITORY_CONFIG_TYPES: &'static [&dyn RepositoryConfigType] = &[ &ProjectConfigType, &RepositoryPageType, &MavenRepositoryConfigType, + &NPMRegistryConfigType, ]; pub static REPOSITORY_TYPES: &'static [&dyn RepositoryType] = &[&MavenRepositoryType, &NpmRegistryType]; diff --git a/nitro_repo/src/error/bad_requests.rs b/nitro_repo/src/error/bad_requests.rs index ef180f00..8f530a4c 100644 --- a/nitro_repo/src/error/bad_requests.rs +++ b/nitro_repo/src/error/bad_requests.rs @@ -4,6 +4,8 @@ use axum::response::Response; use http::header::ToStrError; use http::StatusCode; use thiserror::Error; + +use super::IntoErrorResponse; #[derive(Debug, Error)] pub enum BadRequestErrors { #[error("Could not Decode Base64: {0}")] @@ -20,6 +22,15 @@ pub enum BadRequestErrors { Other(String), #[error(transparent)] Axum(#[from] axum::Error), + #[error("Missing Header: {0}")] + MissingHeader(&'static str), + #[error("Invalid Json Request: {0}")] + InvalidJson(#[from] serde_json::Error), +} +impl IntoErrorResponse for BadRequestErrors { + fn into_response_boxed(self: Box) -> axum::response::Response { + self.into_response() + } } #[derive(Debug, Error)] pub enum InvalidAuthorizationHeader { diff --git a/nitro_repo/src/error/mod.rs b/nitro_repo/src/error/mod.rs index c85a24d8..734f0eb7 100644 --- a/nitro_repo/src/error/mod.rs +++ b/nitro_repo/src/error/mod.rs @@ -1,7 +1,10 @@ mod bad_requests; mod internal_error; +use std::error::Error; + use axum::response::IntoResponse; pub use bad_requests::*; +use derive_more::derive::From; pub use internal_error::*; #[derive(Debug, thiserror::Error)] #[error("Illegal State: {0}")] @@ -14,3 +17,37 @@ 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 { + axum::response::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .body( + format!( + "Internal Service Error. Please Contact the System Admin. Error: {}", + self.0 + ) + .into(), + ) + .unwrap() + } +} + +pub trait IntoErrorResponse: Error + Send + Sync { + fn into_response_boxed(self: Box) -> axum::response::Response; +} +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(), + ) + .unwrap() + } +} diff --git a/nitro_repo/src/repository/maven/mod.rs b/nitro_repo/src/repository/maven/mod.rs index 885f329d..9c857aec 100644 --- a/nitro_repo/src/repository/maven/mod.rs +++ b/nitro_repo/src/repository/maven/mod.rs @@ -6,16 +6,14 @@ use axum::response::IntoResponse; use futures::future::BoxFuture; use hosted::MavenHosted; use nr_core::{ - database::{ - project::{NewProjectBuilderError, NewVersionBuilderError}, - repository::{DBRepository, DBRepositoryConfig}, - }, + builder_error, + database::repository::{DBRepository, DBRepositoryConfig}, repository::{ config::{ project::ProjectConfigType, ConfigDescription, PushRulesConfigType, RepositoryConfigError, RepositoryConfigType, SecurityConfigType, }, - project::{ReleaseType, VersionDataBuilderError}, + project::ReleaseType, }, storage::StoragePath, }; @@ -211,8 +209,8 @@ pub enum MavenError { XMLDeserialize(#[from] maven_rs::quick_xml::DeError), #[error("Database Error: {0}")] Database(#[from] sqlx::Error), - #[error("Internal Error. This is a bug in the code: {0}")] - InternalBuilderError(String), + #[error("Internal Error. {0}")] + BuilderError(#[from] builder_error::BuilderError), #[error("Missing From Pom: {0}")] MissingFromPom(&'static str), #[error("Failed to proxy request {0}")] @@ -220,19 +218,15 @@ pub enum MavenError { #[error(transparent)] BadRequest(#[from] BadRequestErrors), } -impl From for MavenError { - fn from(e: NewProjectBuilderError) -> Self { - MavenError::InternalBuilderError(e.to_string()) - } -} -impl From for MavenError { - fn from(e: NewVersionBuilderError) -> Self { - MavenError::InternalBuilderError(e.to_string()) + +impl IntoErrorResponse for MavenError { + fn into_response_boxed(self: Box) -> axum::response::Response { + self.into_response() } } -impl From for MavenError { - fn from(e: VersionDataBuilderError) -> Self { - MavenError::InternalBuilderError(e.to_string()) +impl From for RepositoryHandlerError { + fn from(e: MavenError) -> Self { + RepositoryHandlerError::Other(Box::new(e)) } } diff --git a/nitro_repo/src/repository/maven/proxy.rs b/nitro_repo/src/repository/maven/proxy.rs index 346bf67e..f19b2832 100644 --- a/nitro_repo/src/repository/maven/proxy.rs +++ b/nitro_repo/src/repository/maven/proxy.rs @@ -138,7 +138,7 @@ impl MavenProxy { for file in project_download_files(&pom)? { debug!(?file, "Downloading file"); let mut path = version_dir.clone(); - path.push(&file); + path.push_mut(&file); let url = format!("{}/{}", proxy_config.url, path); match http_client.get(&url).send().await { Ok(ok) => { diff --git a/nitro_repo/src/repository/maven/utils.rs b/nitro_repo/src/repository/maven/utils.rs index 098944bb..5ff6bd07 100644 --- a/nitro_repo/src/repository/maven/utils.rs +++ b/nitro_repo/src/repository/maven/utils.rs @@ -15,10 +15,7 @@ use tracing::{error, info, instrument, trace}; use uuid::Uuid; use super::{MavenError, RepoResponse, RepositoryAuthentication, RepositoryHandlerError}; -use crate::{ - error::{self, BadRequestErrors}, - repository::Repository, -}; +use crate::{error::BadRequestErrors, repository::Repository}; /// Utilities for Maven Repositories pub trait MavenRepositoryExt: Repository + Debug { @@ -214,6 +211,7 @@ pub fn pom_to_update_db_project_version(pom: Pom) -> Result DynStorage; /// The Repository type. This is used to identify the Repository type in the database fn get_type(&self) -> &'static str; @@ -144,7 +151,7 @@ pub trait Repository: Send + Sync + Clone { #[derive(Debug, Clone, DynRepositoryHandler)] pub enum DynRepository { Maven(maven::MavenRepository), - NPM(npm::NpmRegistry), + NPM(npm::NPMRegistry), } #[derive(Debug, Error)] pub enum RepositoryHandlerError { @@ -156,15 +163,20 @@ pub enum RepositoryHandlerError { MissingBody, #[error("Invalid JSON: {0}")] InvalidJson(#[from] serde_json::Error), - #[error("Bad Request: {0}")] - BadRequest(#[from] BadRequestErrors), - #[error("Maven Repository Error: {0}")] - MavenError(#[from] maven::MavenError), #[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 { @@ -175,8 +187,7 @@ impl IntoResponse for RepositoryHandlerError { error ))) .unwrap(), - RepositoryHandlerError::MavenError(error) => error.into_response(), - RepositoryHandlerError::BadRequest(error) => error.into_response(), + RepositoryHandlerError::Other(error) => error.into_response_boxed(), other => Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .body(Body::from(format!( diff --git a/nitro_repo/src/repository/npm/configs.rs b/nitro_repo/src/repository/npm/configs.rs new file mode 100644 index 00000000..0c005f7c --- /dev/null +++ b/nitro_repo/src/repository/npm/configs.rs @@ -0,0 +1,48 @@ +use nr_core::repository::config::{ConfigDescription, RepositoryConfigError, RepositoryConfigType}; +use schemars::{schema_for, JsonSchema}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type", content = "config")] + +pub enum NPMRegistryConfig { + Hosted, +} + +#[derive(Debug, Clone, Default)] +pub struct NPMRegistryConfigType; +impl RepositoryConfigType for NPMRegistryConfigType { + fn get_type(&self) -> &'static str { + "npm" + } + + fn get_type_static() -> &'static str + where + Self: Sized, + { + "npm" + } + fn schema(&self) -> Option { + Some(schema_for!(NPMRegistryConfig)) + } + fn validate_config(&self, config: Value) -> Result<(), RepositoryConfigError> { + let config: NPMRegistryConfig = serde_json::from_value(config)?; + Ok(()) + } + fn validate_change(&self, old: Value, new: Value) -> Result<(), RepositoryConfigError> { + Ok(()) + } + fn default(&self) -> Result { + let config = NPMRegistryConfig::Hosted; + Ok(serde_json::to_value(config).unwrap()) + } + fn get_description(&self) -> ConfigDescription { + ConfigDescription { + name: "NPM Registry Config", + description: Some("Handles the type of NPM Registry"), + documentation_link: None, + ..Default::default() + } + } +} diff --git a/nitro_repo/src/repository/npm/hosted.rs b/nitro_repo/src/repository/npm/hosted.rs new file mode 100644 index 00000000..ad74168d --- /dev/null +++ b/nitro_repo/src/repository/npm/hosted.rs @@ -0,0 +1,317 @@ +use super::types::{ + request::{GetPath, InvalidNPMCommand, NPMCommand, PublishVersion}, + NpmRegistryPackageResponse, NPM_COMMAND_HEADER, +}; +use super::utils::{npm_time, NpmRegistryExt}; +use crate::{ + app::{responses::no_content_response, NitroRepo}, + repository::{ + npm::{types::PublishRequest, NPMRegistryConfigType, NPMRegistryError}, + utils::RepositoryExt, + RepoResponse, Repository, RepositoryFactoryError, RepositoryHandlerError, + RepositoryRequest, + }, +}; +use ahash::{HashMap, HashMapExt}; +use axum::response::{IntoResponse, Response}; +use derive_more::derive::Deref; +use http::{header::CONTENT_TYPE, StatusCode}; +use nr_core::{ + database::{project::DBProjectVersion, repository::DBRepository}, + repository::config::RepositoryConfigType, + storage::StoragePath, + user::permissions::RepositoryActions, +}; +use nr_storage::{DynStorage, FileContent, Storage}; +use std::sync::Arc; +use tracing::{debug, info, instrument, warn}; + +#[derive(derive_more::Debug)] +pub struct NpmRegistryInner { + #[debug(skip)] + pub site: NitroRepo, + pub storage: DynStorage, + pub id: uuid::Uuid, + pub repository: DBRepository, +} +#[derive(Debug, Clone, Deref)] +pub struct NPMHostedRegistry(Arc); +impl NPMHostedRegistry { + pub async fn load( + site: NitroRepo, + storage: DynStorage, + repository: DBRepository, + ) -> Result { + Ok(Self(Arc::new(NpmRegistryInner { + site, + storage, + id: repository.id, + repository, + }))) + } + #[instrument(name = "NpmRegistry::handle_publish", fields(npm_command = "publish"))] + async fn handle_publish( + &self, + request: RepositoryRequest, + ) -> Result { + let Some(user) = request + .authentication + .get_user_if_has_action(RepositoryActions::Write, self.id, self.site.as_ref()) + .await? + else { + info!("No acceptable user authentication provided"); + return Ok(RepoResponse::unauthorized()); + }; + let body = request.body.body_as_string().await?; + debug!(?body, "Handling publish request"); + let PublishRequest { + name, + attachments, + versions, + other, + }: PublishRequest = serde_json::from_str(&body)?; + if versions.len() != 1 { + return Err(NPMRegistryError::OnlyOneReleaseOrAttachmentAtATime.into()); + } + let (version, data) = versions.into_iter().next().unwrap(); + { + let storage_config: nr_storage::BorrowedStorageConfig = self.storage.storage_config(); + data.dist.validate_tarball( + &storage_config.storage_config.storage_name, + &self.repository.name, + )?; + } + let project_path = StoragePath::from(name.clone()); + let project = self.get_or_create_project(&project_path, &data).await?; + let mut version_path = project_path.clone(); + version_path.push_mut(&version); + + self.create_or_update_version(user.id, &version_path, &project, &data) + .await?; + + for (file, attachment) in attachments.into_iter() { + info!(?file, ?attachment, "Saving Attachment"); + let mut path = version_path.clone(); + if file.starts_with("@") && file.contains("/") { + let split = file.split("/").collect::>(); + path.push_mut(split.last().unwrap()); + } else { + path.push_mut(&file); + } + let attachment_data = attachment.read_data()?; + let storage = self.get_storage(); + storage + .save_file(self.id, FileContent::Content(attachment_data), &path) + .await?; + } + + Ok(no_content_response().into()) + } +} +impl NpmRegistryExt for NPMHostedRegistry {} +impl RepositoryExt for NPMHostedRegistry {} +impl Repository for NPMHostedRegistry { + fn get_storage(&self) -> DynStorage { + self.0.storage.clone() + } + fn site(&self) -> NitroRepo { + self.0.site.clone() + } + + fn get_type(&self) -> &'static str { + "npm" + } + + fn config_types(&self) -> Vec<&str> { + vec![NPMRegistryConfigType::get_type_static()] + } + + fn name(&self) -> String { + self.0.repository.name.to_string() + } + + fn id(&self) -> uuid::Uuid { + self.id + } + + fn visibility(&self) -> nr_core::repository::Visibility { + nr_core::repository::Visibility::Public + } + + fn is_active(&self) -> bool { + true + } + #[instrument(name = "NpmRegistry::handle_get", fields(repository_id = %self.id, repository_name = %self.repository.name))] + async fn handle_get( + &self, + request: RepositoryRequest, + ) -> Result { + let headers = request.headers(); + let path_as_string = request.path.to_string(); + debug!(?headers, ?path_as_string, "Handling NPM GET request"); + let get_path = match GetPath::try_from(request.path.clone()) { + Ok(ok) => ok, + Err(err) => return Ok(err.into_response().into()), + }; + match get_path { + GetPath::GetPackageInfo { name } => { + let Some(project) = self.get_project_from_key(&name).await? else { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(format!("Project {} not found in repository", name).into()) + .into()); + }; + debug!(?project, "Got project"); + let versions = + DBProjectVersion::get_all_versions(project.id, self.site.as_ref()).await?; + let mut dist_tags = HashMap::new(); + let mut times = HashMap::new(); + times.insert( + "created".to_owned(), + npm_time::format_date_time(&project.created_at), + ); + times.insert( + "modified".to_owned(), + npm_time::format_date_time(&project.updated_at), + ); + if let Some(latest) = project.latest_release { + dist_tags.insert("latest".to_string(), latest); + } + let mut versions_map = HashMap::new(); + for version in versions { + times.insert( + version.version.clone(), + npm_time::format_date_time(&version.created_at), + ); + debug!(?version, "Got Version"); + if let Some(extra) = version.extra.0.extra { + let extra: PublishVersion = match serde_json::from_value(extra) { + Ok(ok) => ok, + Err(err) => { + warn!("Invalid NPM Project"); + continue; + } + }; + versions_map.insert(version.version.clone(), extra); + } else { + warn!(?version, "Invalid NPM Project"); + } + } + let project_response = NpmRegistryPackageResponse { + id: project.project_key.clone(), + name: project.name.clone(), + description: project.description.clone(), + dist_tags: dist_tags, + versions: versions_map, + time: times, + }; + debug!(?project_response, "Returning Project"); + let as_string = serde_json::to_string(&project_response).unwrap(); + return Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/json") + .body(as_string.into()) + .into()); + } + GetPath::VersionInfo { name, version } => { + let Some(project) = self.get_project_from_key(&name).await? else { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(format!("Project {} not found in repository", name).into()) + .into()); + }; + debug!(?project, ?version, "Getting version"); + let Some(version) = self.get_project_version(project.id, &version).await? else { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(format!("Version {} not found in project {}", version, name).into()) + .into()); + }; + debug!(?version, "Got Version"); + if let Some(extra) = version.extra.0.extra { + let as_string = serde_json::to_string(&extra).unwrap(); + return Ok(Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/json") + .body(as_string.into()) + .into()); + } else { + return Ok(Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Invalid NPM Project".into()) + .into()); + } + } + GetPath::GetTar { + name, + version, + file, + } => { + let Some(project) = self.get_project_from_key(&name).await? else { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(format!("Project {} not found in repository", name).into()) + .into()); + }; + debug!(?project, ?version, "Getting version"); + let Some(version) = self.get_project_version(project.id, &version).await? else { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body(format!("Version {} not found in project {}", version, name).into()) + .into()); + }; + debug!(?version, "Got Version"); + let mut storage_path = StoragePath::from(version.version_path.as_str()); + storage_path.push_mut(&file); + debug!(?storage_path, "Getting file"); + let storage = self.get_storage(); + let file = storage.open_file(self.id, &storage_path).await?; + return Ok(RepoResponse::from(file)); + } + _ => { + return Ok(Response::builder() + .status(StatusCode::NOT_FOUND) + .body("Not Found".into()) + .into()); + } + } + } + + #[instrument(name = "NpmRegistry::handle_put")] + async fn handle_put( + &self, + request: RepositoryRequest, + ) -> Result { + let path_as_string = request.path.to_string(); + debug!( + ?path_as_string, + headers = ?request.headers(), + ); + if path_as_string.starts_with(r#"-/user/org.couchdb.user:"#) { + return super::login::couch_db::perform_login(self, request).await; + } else if path_as_string.eq("-/v1/login") { + return super::login::web_login::perform_login(self, request).await; + } + let Some(user) = request + .authentication + .get_user_if_has_action(RepositoryActions::Write, self.id, self.site.as_ref()) + .await? + else { + info!("No acceptable user authentication provided"); + return Ok(RepoResponse::unauthorized()); + }; + let command_header = match request + .headers() + .get(NPM_COMMAND_HEADER) + .ok_or(InvalidNPMCommand::NoHeaderFound) + .and_then(|x| NPMCommand::try_from(x)) + { + Ok(ok) => ok, + Err(err) => return Ok(err.into_response().into()), + }; + + match command_header { + NPMCommand::Publish => self.handle_publish(request).await, + } + } +} diff --git a/nitro_repo/src/repository/npm/login/couch_db.rs b/nitro_repo/src/repository/npm/login/couch_db.rs new file mode 100644 index 00000000..01f54b16 --- /dev/null +++ b/nitro_repo/src/repository/npm/login/couch_db.rs @@ -0,0 +1,79 @@ +use std::fmt::Debug; + +use chrono::{DateTime, FixedOffset}; +use derive_more::derive::From; +use nr_core::{ + database::user::auth_token::NewRepositoryToken, user::permissions::RepositoryActions, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tracing::{debug, instrument}; + +use crate::{ + app::authentication::verify_login, + repository::{ + npm::{login::LoginResponse, utils::NpmRegistryExt}, + RepoResponse, RepositoryHandlerError, RepositoryRequest, + }, +}; + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct CouchDBLoginRequest { + pub name: String, + pub password: String, + pub email: Option, + #[serde(rename = "type")] + pub login_type: String, + #[serde(default)] + pub roles: Vec, + //#[serde(with = "nr_core::utils::time::iso_8601")] + //pub date: DateTime, +} +impl Debug for CouchDBLoginRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CouchDBLogin") + .field("name", &self.name) + .field("password", &"********") + .field("email", &self.email) + .field("login_type", &self.login_type) + .field("roles", &self.roles) + // .field("date", &self.date) + .finish() + } +} +#[derive(Debug, Serialize, Deserialize, From)] +pub struct CouchDBLoginResponse { + pub token: String, +} +/// Handles the login request for CouchDB +/// Required route is `/-/user/org.couchdb.user:` +#[instrument(name = "npm_couch_db_login")] +pub async fn perform_login( + repository: &impl NpmRegistryExt, + request: RepositoryRequest, +) -> Result { + let path_as_string = request.path.to_string(); + let Some(source) = request + .user_agent_as_string()? + .map(|header| format!("NPM CLI ({})", header)) + else { + return Ok(RepoResponse::forbidden()); + }; + let user_name = path_as_string.replace("-/user/org.couchdb.user:", ""); + let body = request.body.body_as_string().await?; + debug!(?user_name, ?body, "Handling PUT request"); + let login: CouchDBLoginRequest = serde_json::from_str(&body)?; + debug!(?login, "Handling PUT request"); + let user = match verify_login(login.name, login.password, repository.site().as_ref()).await { + Ok(ok) => ok, + Err(err) => { + return Ok(RepoResponse::forbidden()); + } + }; + + let (_, token) = + NewRepositoryToken::new(user.id, source, repository.id(), RepositoryActions::all()) + .insert(repository.site().as_ref()) + .await?; + return Ok(LoginResponse::ValidCouchDBLogin(CouchDBLoginResponse::from(token)).into()); +} diff --git a/nitro_repo/src/repository/npm/login/mod.rs b/nitro_repo/src/repository/npm/login/mod.rs new file mode 100644 index 00000000..8d8e2b28 --- /dev/null +++ b/nitro_repo/src/repository/npm/login/mod.rs @@ -0,0 +1,43 @@ +use axum::response::{IntoResponse, Response}; +use couch_db::CouchDBLoginResponse; +use derive_more::derive::From; +use http::StatusCode; +use serde::{Deserialize, Serialize}; + +use crate::repository::RepoResponse; +pub mod couch_db; +pub mod web_login; +#[derive(Debug, Serialize, Deserialize)] +pub struct NewLoginResponse { + pub done_url: String, + pub login_url: String, +} +#[derive(Debug, From)] +pub enum LoginResponse { + ValidCouchDBLogin(CouchDBLoginResponse), + UnsupportedLogin, +} + +impl IntoResponse for LoginResponse { + fn into_response(self) -> axum::response::Response { + match self { + LoginResponse::ValidCouchDBLogin(login) => { + return Response::builder() + .status(StatusCode::CREATED) + .body(serde_json::to_string(&login).unwrap().into()) + .unwrap(); + } + LoginResponse::UnsupportedLogin => { + return Response::builder() + .status(StatusCode::IM_A_TEAPOT) + .body("Unsupported Login Type".into()) + .unwrap(); + } + } + } +} +impl From for RepoResponse { + fn from(value: LoginResponse) -> Self { + RepoResponse::Other(value.into_response()) + } +} diff --git a/nitro_repo/src/repository/npm/login/web_login.rs b/nitro_repo/src/repository/npm/login/web_login.rs new file mode 100644 index 00000000..ecf1ca4a --- /dev/null +++ b/nitro_repo/src/repository/npm/login/web_login.rs @@ -0,0 +1,12 @@ +use crate::repository::{ + npm::utils::NpmRegistryExt, RepoResponse, RepositoryHandlerError, RepositoryRequest, +}; + +use super::LoginResponse; + +pub async fn perform_login( + repository: &impl NpmRegistryExt, + request: RepositoryRequest, +) -> Result { + return Ok(LoginResponse::UnsupportedLogin.into()); +} diff --git a/nitro_repo/src/repository/npm/mod.rs b/nitro_repo/src/repository/npm/mod.rs index e99a6e54..14738699 100644 --- a/nitro_repo/src/repository/npm/mod.rs +++ b/nitro_repo/src/repository/npm/mod.rs @@ -1,130 +1,171 @@ //! NPM Registry Implementation //! //! Documentation for NPM: https://github.com/npm/registry/blob/main/docs/REGISTRY-API.md +//! -use std::sync::Arc; +use std::borrow::Cow; -use crate::{ - app::{authentication::verify_login, responses::no_content_response, NitroRepo}, - repository::RepoResponse, -}; -use derive_more::derive::Deref; -use nr_core::{ - database::{repository::DBRepository, user::auth_token::NewRepositoryToken}, - user::permissions::RepositoryActions, -}; +use ahash::HashMap; +use base64::DecodeError; +use config::RepositoryConfigType; +use futures::future::BoxFuture; +use hosted::NPMHostedRegistry; +use nr_core::database::repository::{DBRepository, DBRepositoryConfig}; +use nr_macros::DynRepositoryHandler; use nr_storage::DynStorage; -use tracing::{debug, instrument}; -use types::login::{CouchDBLoginRequest, CouchDBLoginResponse, LoginResponse}; +use tracing::debug; +use types::InvalidNPMPackageName; -use super::{Repository, RepositoryFactoryError}; -pub mod registry_type; +pub mod hosted; +pub mod login; pub mod types; -#[derive(derive_more::Debug)] -pub struct NpmRegistryInner { - #[debug(skip)] - pub site: NitroRepo, - pub storage: DynStorage, - pub id: uuid::Uuid, - pub repository: DBRepository, +pub mod utils; +use crate::error::{IntoErrorResponse, SQLXError}; + +pub use super::prelude::*; +mod configs; +use super::{ + DynRepository, NewRepository, RepositoryFactoryError, RepositoryType, RepositoryTypeDescription, +}; +pub use configs::*; + +#[derive(Debug, Clone, DynRepositoryHandler)] +pub enum NPMRegistry { + Hosted(hosted::NPMHostedRegistry), } -#[derive(Debug, Clone, Deref)] -pub struct NpmRegistry(Arc); -impl NpmRegistry { - pub async fn load( - site: NitroRepo, - storage: DynStorage, - repository: DBRepository, - ) -> Result { - Ok(Self(Arc::new(NpmRegistryInner { - site, - storage, - id: repository.id, - repository, - }))) + +#[derive(Debug, thiserror::Error)] +pub enum NPMRegistryError { + #[error(transparent)] + DatabaseError(#[from] sqlx::Error), + #[error(transparent)] + InvalidName(#[from] InvalidNPMPackageName), + #[error( + "Invalid tarball. The tarballs location is invalid. + This means you used `$BASE_URL/repositories/$STORAGE/$REPO` without a trailing slash. + tarbar Route: {tarball_route} Error: {error}" + )] + InvalidTarball { + tarball_route: String, + error: Cow<'static, str>, + }, + #[error("Invalid GET request. The requested route is invalid to the NPM Registry. This could be a bug. AS the code is very sketchy")] + InvalidGetRequest, + #[error("Invalid Package Attachment. Error: {0}")] + InvalidPackageAttachment(DecodeError), + #[error("Only one release or attachment can be uploaded at a time")] + OnlyOneReleaseOrAttachmentAtATime, +} +impl IntoErrorResponse for NPMRegistryError { + fn into_response_boxed(self: Box) -> axum::response::Response { + self.into_response() } } -impl Repository for NpmRegistry { - fn get_storage(&self) -> DynStorage { - self.0.storage.clone() + +impl From for RepositoryHandlerError { + fn from(err: NPMRegistryError) -> Self { + RepositoryHandlerError::Other(Box::new(err)) } - fn site(&self) -> NitroRepo { - self.0.site.clone() +} + +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(), + bad_request => { + debug!("Bad Request: {:?}", bad_request); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(bad_request.to_string().into()) + .unwrap() + } + } } +} +#[derive(Debug, Default)] +pub struct NpmRegistryType; +impl RepositoryType for NpmRegistryType { fn get_type(&self) -> &'static str { "npm" } fn config_types(&self) -> Vec<&str> { - vec![] - } - - fn name(&self) -> String { - self.0.repository.name.to_string() + vec![NPMRegistryConfigType::get_type_static()] } - fn id(&self) -> uuid::Uuid { - self.id - } - - fn visibility(&self) -> nr_core::repository::Visibility { - nr_core::repository::Visibility::Public - } - - fn is_active(&self) -> bool { - true - } - - #[instrument(name = "NpmRegistry::handle_post")] - async fn handle_post( - &self, - request: super::RepositoryRequest, - ) -> Result { - let headers = request.headers(); - debug!(?headers, "Handling POST request"); - let path_as_string = request.path.to_string(); - if path_as_string.starts_with(r#"-/user/org\.couchdb\.user:"#) { - let user_name = path_as_string.replace("-/user/org.couchdb.user:", ""); + fn get_description(&self) -> RepositoryTypeDescription { + RepositoryTypeDescription { + type_name: "npm", + name: "NPM", + description: "A NPM Registry", + documentation_url: None, + is_stable: true, + required_configs: vec![NPMRegistryConfigType::get_type_static()], } - Ok(no_content_response().into()) } - #[instrument(name = "NpmRegistry::handle_put")] - async fn handle_put( - &self, - request: super::RepositoryRequest, - ) -> Result { - let headers = request.headers(); - debug!(?headers, "Handling PUT request"); - let path_as_string = request.path.to_string(); - debug!(?path_as_string, "Handling PUT request"); - if path_as_string.starts_with(r#"-/user/org.couchdb.user:"#) { - let user_name = path_as_string.replace("-/user/org.couchdb.user:", ""); - let body = request.body.body_as_string().await?; - debug!(?user_name, ?body, "Handling PUT request"); - let login: CouchDBLoginRequest = serde_json::from_str(&body)?; - debug!(?login, "Handling PUT request"); - let message = format!("user '{}' created", login.name); - let user = match verify_login(login.name, login.password, self.site.as_ref()).await { + fn create_new( + &self, + name: String, + uuid: uuid::Uuid, + configs: HashMap, + storage: nr_storage::DynStorage, + ) -> BoxFuture<'static, Result> { + Box::pin(async move { + let sub_type = configs + .get(NPMRegistryConfigType::get_type_static()) + .ok_or(RepositoryFactoryError::MissingConfig( + NPMRegistryConfigType::get_type_static(), + ))? + .clone(); + let maven_config: NPMRegistryConfig = match serde_json::from_value(sub_type) { Ok(ok) => ok, Err(err) => { - return Ok(RepoResponse::forbidden()); + return Err(RepositoryFactoryError::InvalidConfig( + NPMRegistryConfigType::get_type_static(), + err.to_string(), + )); } }; - let (_, token) = NewRepositoryToken::new( - user.id, - "NPM CLI".to_owned(), - self.id, - RepositoryActions::all(), + Ok(NewRepository { + name, + uuid, + repository_type: "npm".to_string(), + configs, + }) + }) + } + + fn load_repo( + &self, + repo: DBRepository, + storage: DynStorage, + website: NitroRepo, + ) -> BoxFuture<'static, Result> { + Box::pin(async move { + let Some(npm_config_db) = DBRepositoryConfig::::get_config( + repo.id, + NPMRegistryConfigType::get_type_static(), + &website.database, ) - .insert(self.site.as_ref()) - .await?; - return Ok(LoginResponse::ValidCouchDBLogin(CouchDBLoginResponse::from(token)).into()); - } else if path_as_string.eq("/-/v1/login") { - // TODO: Implement the new login system - return Ok(LoginResponse::UnsupportedLogin.into()); - } - Ok(no_content_response().into()) + .await? + else { + return Err(RepositoryFactoryError::MissingConfig( + NPMRegistryConfigType::get_type_static(), + )); + }; + let npm_config = npm_config_db.value.0; + match npm_config { + NPMRegistryConfig::Hosted => { + let maven_hosted = NPMHostedRegistry::load(website, storage, repo).await?; + Ok(NPMRegistry::Hosted(maven_hosted).into()) + } + } + }) } } diff --git a/nitro_repo/src/repository/npm/registry_type.rs b/nitro_repo/src/repository/npm/registry_type.rs deleted file mode 100644 index dc92bde0..00000000 --- a/nitro_repo/src/repository/npm/registry_type.rs +++ /dev/null @@ -1,70 +0,0 @@ -use ahash::HashMap; -use futures::future::BoxFuture; -use nr_core::database::repository::DBRepository; -use nr_storage::DynStorage; - -use crate::{ - app::NitroRepo, - repository::{ - DynRepository, NewRepository, RepositoryFactoryError, RepositoryType, - RepositoryTypeDescription, - }, -}; - -use super::NpmRegistry; - -#[derive(Debug, Default)] -pub struct NpmRegistryType; - -impl RepositoryType for NpmRegistryType { - fn get_type(&self) -> &'static str { - "npm" - } - - fn config_types(&self) -> Vec<&str> { - vec![] - } - - fn get_description(&self) -> RepositoryTypeDescription { - RepositoryTypeDescription { - type_name: "npm", - name: "NPM", - description: "A NPM Registry", - documentation_url: None, - is_stable: true, - required_configs: vec![], - } - } - - fn create_new( - &self, - name: String, - uuid: uuid::Uuid, - configs: HashMap, - storage: nr_storage::DynStorage, - ) -> BoxFuture<'static, Result> { - Box::pin(async move { - Ok(NewRepository { - name, - uuid, - repository_type: "npm".to_string(), - configs, - }) - }) - } - - #[doc = " Load a repository from the database"] - #[doc = " This function should load the repository from the database and return a DynRepository"] - fn load_repo( - &self, - repo: DBRepository, - storage: DynStorage, - website: NitroRepo, - ) -> BoxFuture<'static, Result> { - Box::pin(async move { - NpmRegistry::load(website, storage, repo) - .await - .map(DynRepository::from) - }) - } -} diff --git a/nitro_repo/src/repository/npm/types/login.rs b/nitro_repo/src/repository/npm/types/login.rs deleted file mode 100644 index 75aef611..00000000 --- a/nitro_repo/src/repository/npm/types/login.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::fmt::Debug; - -use axum::response::{IntoResponse, Response}; -use chrono::{DateTime, FixedOffset}; -use derive_more::derive::{AsRef, Deref, From}; -use http::StatusCode; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -use crate::repository::RepoResponse; -pub static ISO_8601: &str = "%Y-%m-%dT%H:%M:%S.%f"; -#[derive(Serialize, Deserialize)] -pub struct CouchDBLoginRequest { - pub name: String, - pub password: String, - pub email: String, - #[serde(rename = "type")] - pub login_type: String, - #[serde(default)] - pub roles: Vec, - pub date: DateTime, -} -#[derive(Debug, From, AsRef, Deref)] -pub struct CouchDBTime(DateTime); -impl Serialize for CouchDBTime { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - self.0.format(ISO_8601).to_string().serialize(serializer) - } -} -impl<'de> Deserialize<'de> for CouchDBTime { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - - DateTime::parse_from_str(&s, ISO_8601) - .map_err(serde::de::Error::custom) - .map(CouchDBTime::from) - } -} -impl Debug for CouchDBLoginRequest { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CouchDBLogin") - .field("name", &self.name) - .field("password", &"********") - .field("email", &self.email) - .field("login_type", &self.login_type) - .field("roles", &self.roles) - .field("date", &self.date) - .finish() - } -} -#[derive(Debug, Serialize, Deserialize, From)] -pub struct CouchDBLoginResponse { - pub token: String, -} -#[derive(Debug, Serialize, Deserialize)] -pub struct NewLoginResponse { - pub done_url: String, - pub login_url: String, -} -pub enum LoginResponse { - ValidCouchDBLogin(CouchDBLoginResponse), - UnsupportedLogin, -} - -impl IntoResponse for LoginResponse { - fn into_response(self) -> axum::response::Response { - match self { - LoginResponse::ValidCouchDBLogin(login) => { - return Response::builder() - .status(StatusCode::CREATED) - .body(serde_json::to_string(&login).unwrap().into()) - .unwrap(); - } - LoginResponse::UnsupportedLogin => { - return Response::builder() - .status(StatusCode::IM_A_TEAPOT) - .body("Unsupported Login Type".into()) - .unwrap(); - } - } - } -} -impl From for RepoResponse { - fn from(value: LoginResponse) -> Self { - RepoResponse::Generic(value.into_response()) - } -} diff --git a/nitro_repo/src/repository/npm/types/mod.rs b/nitro_repo/src/repository/npm/types/mod.rs index 6b36cf00..3c4abf81 100644 --- a/nitro_repo/src/repository/npm/types/mod.rs +++ b/nitro_repo/src/repository/npm/types/mod.rs @@ -1,11 +1,16 @@ pub mod request; -pub mod login; use ahash::HashMap; use chrono::{DateTime, FixedOffset}; +use http::HeaderName; +use request::PublishVersion; use serde::{Deserialize, Serialize}; use serde_json::Value; - +mod name; +mod publish; +pub use name::{InvalidNPMPackageName, NPMPackageName}; +pub use publish::*; +pub const NPM_COMMAND_HEADER: HeaderName = HeaderName::from_static("npm-command"); #[derive(Debug, Clone)] pub struct RegistryResponse { pub db_name: String, @@ -21,13 +26,11 @@ pub struct RegistryResponse { pub struct NpmRegistryPackageResponse { #[serde(rename = "_id")] pub id: String, - #[serde(rename = "_rev")] - pub rev: String, pub name: String, pub description: Option, #[serde(rename = "dist-tags")] pub dist_tags: HashMap, - pub versions: HashMap, + pub versions: HashMap, pub time: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -46,3 +49,12 @@ pub struct Maintainers { pub struct Bugs { pub url: String, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PackageFile { + pub name: String, + pub version: String, + pub main: Option, + pub module: Option, + #[serde(flatten)] + pub other: HashMap, +} diff --git a/nitro_repo/src/repository/npm/types/name.rs b/nitro_repo/src/repository/npm/types/name.rs new file mode 100644 index 00000000..7690e6cb --- /dev/null +++ b/nitro_repo/src/repository/npm/types/name.rs @@ -0,0 +1,162 @@ +use std::fmt::Display; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use tracing::instrument; + +#[derive(Debug, Error)] +#[error("Invalid NPM Package Name: {name} - {reason}")] +pub struct InvalidNPMPackageName { + pub name: String, + pub reason: &'static str, +} +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct NPMPackageName { + pub name: String, + pub scope: Option, +} +impl Display for NPMPackageName { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.scope { + Some(scope) => write!(f, "@{}/{}", scope, self.name), + None => write!(f, "{}", self.name), + } + } +} +impl NPMPackageName { + pub fn validate_name(name: &str) -> Result<(), InvalidNPMPackageName> { + for c in name.chars() { + if !c.is_ascii_alphanumeric() && c != '-' && c != '_' { + return Err(InvalidNPMPackageName { + name: name.to_owned(), + reason: "All characters must be alphanumeric, `_`, or `-`", + }); + } + if c.is_alphabetic() && !c.is_ascii_lowercase() { + return Err(InvalidNPMPackageName { + name: name.to_owned(), + reason: "All characters must be lowercase", + }); + } + } + Ok(()) + } +} +impl TryFrom for NPMPackageName { + type Error = InvalidNPMPackageName; + #[instrument(name = "NPMPackageName::try_from")] + fn try_from(value: String) -> Result { + if value.starts_with('@') { + let parts: Vec<_> = value.split('/').collect(); + if parts.len() != 2 { + return Err(InvalidNPMPackageName { + name: value, + reason: "Invalid scope format. Must be @scope/name", + }); + } + let scope = parts.get(0).map(|s| s.to_string()); + let name = parts.get(1).map(|s| s.to_string()).unwrap(); + NPMPackageName::validate_name(&name)?; + Ok(NPMPackageName { name, scope }) + } else { + NPMPackageName::validate_name(&value)?; + Ok(NPMPackageName { + name: value, + scope: None, + }) + } + } +} +impl TryFrom<&str> for NPMPackageName { + type Error = InvalidNPMPackageName; + fn try_from(value: &str) -> Result { + NPMPackageName::try_from(value.to_owned()) + } +} +impl Serialize for NPMPackageName { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match &self.scope { + Some(scope) => serializer.serialize_str(&format!("@{}/{}", scope, self.name)), + None => serializer.serialize_str(&self.name), + } + } +} +impl<'de> Deserialize<'de> for NPMPackageName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + NPMPackageName::try_from(value).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +pub mod tests { + use core::panic; + + use pretty_assertions::assert_eq; + + use super::NPMPackageName; + #[test] + pub fn valid_packages() { + let valid = vec![ + ( + "test", + NPMPackageName { + name: "test".to_string(), + scope: None, + }, + ), + ( + "test-package", + NPMPackageName { + name: "test-package".to_string(), + scope: None, + }, + ), + ( + "test_package", + NPMPackageName { + name: "test_package".to_string(), + scope: None, + }, + ), + ( + "@scope/test", + NPMPackageName { + name: "test".to_string(), + scope: Some("@scope".to_string()), + }, + ), + ( + "@scope/test-package", + NPMPackageName { + name: "test-package".to_string(), + scope: Some("@scope".to_string()), + }, + ), + ( + "@scope/test_package", + NPMPackageName { + name: "test_package".to_string(), + scope: Some("@scope".to_string()), + }, + ), + ]; + for (package, expected) in valid { + match super::NPMPackageName::try_from(package) { + Ok(ok) => { + assert_eq!(ok, expected); + } + Err(err) => { + eprintln!("Error: {:?}", err); + panic!("Failed to parse package: {} \n error: {err}", package); + } + } + } + } +} diff --git a/nitro_repo/src/repository/npm/types/publish.rs b/nitro_repo/src/repository/npm/types/publish.rs new file mode 100644 index 00000000..65bd35df --- /dev/null +++ b/nitro_repo/src/repository/npm/types/publish.rs @@ -0,0 +1,56 @@ +use std::fmt::Debug; + +use ahash::HashMap; +use base64::{engine::general_purpose::STANDARD, Engine}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::repository::npm::NPMRegistryError; + +use super::request::PublishVersion; + +#[derive(Deserialize, Serialize, Clone, PartialEq)] +pub struct PublishAttachment { + /// Content-Type of the attachment + /// Almost always `application/octet-stream` + pub content_type: String, + /// Raw Data of the attachment + pub data: String, + /// Length of the attachment + pub length: usize, +} +impl Debug for PublishAttachment { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PublishAttachment") + .field("content_type", &self.content_type) + .field("length", &self.length) + .finish() + } +} +impl PublishAttachment { + pub fn read_data(self) -> Result, NPMRegistryError> { + let mut data = Vec::with_capacity(self.length); + STANDARD + .decode_vec(self.data, &mut data) + .map_err(|e| NPMRegistryError::InvalidPackageAttachment(e))?; + Ok(data) + } + pub fn new(data: Vec, content_type: String) -> Self { + let length = data.len(); + let data = STANDARD.encode(data); + Self { + content_type, + data, + length, + } + } +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct PublishRequest { + pub name: String, + pub versions: HashMap, + #[serde(flatten)] + pub other: HashMap, + #[serde(rename = "_attachments")] + pub attachments: HashMap, +} diff --git a/nitro_repo/src/repository/npm/types/request.rs b/nitro_repo/src/repository/npm/types/request.rs index e32a83b4..5eb9dd21 100644 --- a/nitro_repo/src/repository/npm/types/request.rs +++ b/nitro_repo/src/repository/npm/types/request.rs @@ -1,10 +1,325 @@ +use std::{borrow::Cow, str::FromStr}; + use ahash::HashMap; +use axum::response::{IntoResponse, Response}; +use http::{header::ToStrError, HeaderValue}; +use nr_core::{ + database::project::{NewProject, NewVersion}, + repository::project::VersionData, + storage::{StoragePath, StoragePathComponent}, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use strum::{Display, EnumString}; +use tracing::{debug, info}; +use uuid::Uuid; + +use crate::repository::{maven::get_release_type, npm::NPMRegistryError}; + +use super::NPMPackageName; + +#[derive(Debug, Display, EnumString)] +pub enum NPMCommand { + #[strum(serialize = "publish")] + Publish, +} +impl TryFrom<&HeaderValue> for NPMCommand { + type Error = InvalidNPMCommand; + fn try_from(value: &HeaderValue) -> Result { + let value = value.to_str()?; + NPMCommand::from_str(value) + .map_err(|_| InvalidNPMCommand::InvalidCommand(value.to_string())) + } +} +#[derive(Debug, thiserror::Error)] +pub enum InvalidNPMCommand { + #[error("Invalid command {0}")] + InvalidCommand(String), + #[error("Unparsable command {0}")] + UnparsableCommand(#[from] ToStrError), + #[error("No header found")] + NoHeaderFound, +} +impl IntoResponse for InvalidNPMCommand { + fn into_response(self) -> Response { + Response::builder() + .status(http::StatusCode::BAD_REQUEST) + .body(self.to_string().into()) + .unwrap() + } +} +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct PublishVersion { + pub name: NPMPackageName, + pub version: String, + pub dist: PublishDist, + #[serde(flatten)] + pub extra: HashMap, + #[serde(rename = "_id")] + pub hidden_id: String, + #[serde(default)] + pub readme: String, + #[serde(default, rename = "readmeFilename")] + pub readme_file_name: String, + #[serde(rename = "_nodeVersion")] + pub secret_node_version: String, + #[serde(rename = "_npmVersion")] + pub hidden_npm_version: String, +} +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +pub struct PublishDist { + pub integrity: String, + pub shasum: String, + pub tarball: String, + #[serde(flatten)] + pub other: HashMap, +} +impl PublishDist { + #[tracing::instrument] + pub fn validate_tarball( + &self, + storage_name: &str, + repository_name: &str, + ) -> Result<(), NPMRegistryError> { + let url = url::Url::from_str(&self.tarball).map_err(|error| { + info!(?error, "Invalid tarball"); + NPMRegistryError::InvalidTarball { + tarball_route: self.tarball.clone(), + error: Cow::Owned(format!("Invalid URL: {}", error)), + } + })?; + let mut path = url + .path_segments() + .ok_or(NPMRegistryError::InvalidTarball { + tarball_route: self.tarball.clone(), + error: Cow::Borrowed("No Path"), + })?; + if path.next().is_none() { + info!(?url, "Invalid tarball (Missing Base Path for tarball)"); + return Err(NPMRegistryError::InvalidTarball { + tarball_route: self.tarball.clone(), + error: Cow::Borrowed("Missing base path"), + }); + } + if path.next() != Some(storage_name) { + info!(?url, "Invalid tarball (Missing storage name)"); + return Err(NPMRegistryError::InvalidTarball { + tarball_route: self.tarball.clone(), + error: Cow::Borrowed("Missing storage name"), + }); + } + if path.next() != Some(repository_name) { + info!(?url, "Invalid tarball (Missing repository name)"); + return Err(NPMRegistryError::InvalidTarball { + tarball_route: self.tarball.clone(), + error: Cow::Borrowed("Missing repository name"), + }); + } + Ok(()) + } +} +impl PublishVersion { + pub fn new_project( + &self, + save_path: String, + repository_id: Uuid, + ) -> Result { + let project_key = self.name.to_string(); + let NPMPackageName { name, scope } = self.name.clone(); + Ok(NewProject { + scope: scope, + project_key, + name: name, + storage_path: save_path, + repository: repository_id, + latest_pre_release: None, + latest_release: None, + description: None, + tags: vec![], + }) + } + pub fn new_version( + &self, + project_id: Uuid, + save_path: String, + publisher: i32, + ) -> Result { + let release_type = get_release_type(&self.version); + let extra = VersionData { + extra: Some(serde_json::to_value(self).unwrap()), + ..Default::default() + }; + Ok(NewVersion { + project_id, + version: self.version.clone(), + release_type, + version_path: save_path, + publisher: Some(publisher), + version_page: None, + extra: extra, + }) + } +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GetPath { + RegistryBase, + Search, + GetPackageInfo { + name: String, + }, + VersionInfo { + name: String, + version: String, + }, + GetTar { + name: String, + version: String, + file: String, + }, +} +impl GetPath { + /// Path Types + /// + /// - `@{scope}/{package}` - Get package info + /// - `@{scope}/{package}/{version}` - Get version info + /// - `@{scope}/{package}/-/{scope}/{package}-{version}.tgz` - Get file + pub fn scoped_package_call( + components: Vec, + ) -> Result { + let length = components.len(); + if length == 1 { + panic!("Invalid path"); + } + let name = format!( + "{}/{}", + components[0].to_string(), + components[1].to_string() + ); + if length == 2 { + return Ok(GetPath::GetPackageInfo { name }); + } + if length == 3 { + let version = components[2].to_string(); + debug!(?name, ?version, "Version info"); + return Ok(GetPath::VersionInfo { name, version }); + } + if length == 5 { + let file = components[4].to_string(); + let version = + extract_version_from_file(&file).ok_or(NPMRegistryError::InvalidGetRequest)?; + return Ok(GetPath::GetTar { + name, + version, + file, + }); + } + info!(?components, "Invalid path"); + return Err(NPMRegistryError::InvalidGetRequest); + } + /// Path Types + /// + /// - `{package}` - Get package info + /// - `{package}/{version}` - Get version info + /// - `{package}/-/{package}-{version}.tgz` - Get file + pub fn unscoped_package_call( + components: Vec, + ) -> Result { + let length = components.len(); + + let name = components[0].to_string(); + if length == 1 { + return Ok(GetPath::GetPackageInfo { name }); + } + if length == 2 { + let version = components[1].to_string(); + debug!(?name, ?version, "Version info"); + return Ok(GetPath::VersionInfo { name, version }); + } + if length == 3 { + let file = components[2].to_string(); + let version = + extract_version_from_file(&file).ok_or(NPMRegistryError::InvalidGetRequest)?; + return Ok(GetPath::GetTar { + name, + version, + file, + }); + } + info!(?components, "Invalid path"); + return Err(NPMRegistryError::InvalidGetRequest); + } +} +impl TryFrom for GetPath { + type Error = NPMRegistryError; + + fn try_from(value: StoragePath) -> Result { + let as_string = value.to_string(); + let components: Vec<_> = value.into(); + if as_string.starts_with('@') { + return GetPath::scoped_package_call(components); + } else { + return GetPath::unscoped_package_call(components); + } + } +} +pub fn extract_version_from_file(file: &str) -> Option { + let parts: Vec<_> = file.split('-').collect(); + if let Some(version) = parts.last() { + let version = version.trim_end_matches(".tgz"); + return Some(version.to_string()); + } + None +} + +#[cfg(test)] +pub mod tests { + use nr_core::storage::StoragePath; -#[derive(Debug, Serialize, Deserialize)] -pub struct PublishRequest { - pub name: String, - pub _attachments: HashMap, - pub versions: HashMap, + use super::GetPath; + #[test] + pub fn tests() { + let tests = vec![ + ( + StoragePath::from("@nr/mylib/-/@nr/mylib-1.0.0.tgz"), + GetPath::GetTar { + name: "@nr/mylib".to_string(), + version: "1.0.0".to_string(), + file: "mylib-1.0.0.tgz".to_string(), + }, + ), + ( + StoragePath::from("mylib/-/mylib-1.0.0.tgz"), + GetPath::GetTar { + name: "mylib".to_string(), + version: "1.0.0".to_string(), + file: "mylib-1.0.0.tgz".to_string(), + }, + ), + ( + StoragePath::from("mylib/1.0.0"), + GetPath::VersionInfo { + name: "mylib".to_string(), + version: "1.0.0".to_string(), + }, + ), + ( + StoragePath::from("mylib"), + GetPath::GetPackageInfo { + name: "mylib".to_string(), + }, + ), + ( + StoragePath::from("npm-check-updates/-/npm-check-updates-11.0.3.tgz"), + GetPath::GetTar { + name: "npm-check-updates".to_string(), + version: "11.0.3".to_string(), + file: "npm-check-updates-11.0.3.tgz".to_string(), + }, + ), + ]; + for (path, expected) in tests { + let get_path = GetPath::try_from(path).unwrap(); + assert_eq!(get_path, expected); + } + } } diff --git a/nitro_repo/src/repository/npm/utils.rs b/nitro_repo/src/repository/npm/utils.rs new file mode 100644 index 00000000..c8899d16 --- /dev/null +++ b/nitro_repo/src/repository/npm/utils.rs @@ -0,0 +1,75 @@ +use nr_core::{ + database::project::{DBProject, DBProjectVersion, ProjectDBType}, + storage::StoragePath, +}; +use tracing::{info, instrument}; + +use crate::repository::Repository; + +use super::{types::request::PublishVersion, NPMRegistryError}; + +pub mod npm_time { + use chrono::{DateTime, FixedOffset}; + + pub fn format_date_time(date_time: &DateTime) -> String { + date_time.format("%Y-%m-%dT%H:%M:%S.%3fZ").to_string() + } +} +pub trait NpmRegistryExt: Repository { + #[instrument] + async fn get_or_create_project( + &self, + save_path: &StoragePath, + release: &PublishVersion, + ) -> Result { + if let Some(project) = DBProject::find_by_project_key( + &release.name.to_string(), + self.id(), + &self.site().as_ref(), + ) + .await? + { + // TODO: Update + return Ok(project); + } + + match release.new_project(save_path.to_string(), self.id()) { + Ok(ok) => { + let insert = ok.insert(&self.site().as_ref()).await?; + info!(?insert, "Created new project"); + Ok(insert) + } + Err(err) => { + return Err(err); + } + } + } + #[instrument] + async fn create_or_update_version( + &self, + publisher: i32, + save_path: &StoragePath, + project: &DBProject, + release: &PublishVersion, + ) -> Result<(), NPMRegistryError> { + if let Some(version) = DBProjectVersion::find_by_version_and_project( + &release.version, + project.id, + &self.site().database, + ) + .await? + { + return Ok(()); + } + + match release.new_version(project.id, save_path.to_string(), publisher) { + Ok(ok) => { + ok.insert_no_return(&self.site().database).await?; + return Ok(()); + } + Err(err) => { + return Err(err); + } + } + } +} diff --git a/nitro_repo/src/repository/repo_http.rs b/nitro_repo/src/repository/repo_http.rs index 2ffa9e3c..a2b86cc6 100644 --- a/nitro_repo/src/repository/repo_http.rs +++ b/nitro_repo/src/repository/repo_http.rs @@ -18,7 +18,7 @@ use axum::{ use bytes::Bytes; use derive_more::From; use http::{ - header::{CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_TYPE, ETAG, LAST_MODIFIED}, + header::{CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_TYPE, ETAG, LAST_MODIFIED, USER_AGENT}, request::Parts, HeaderValue, Method, StatusCode, }; @@ -43,6 +43,7 @@ impl RepositoryRequestBody { let bytes = body.to_bytes(); Ok(bytes) } + #[cfg(not(debug_assertions))] #[instrument] pub async fn body_as_json Deserialize<'a>>( self, @@ -54,6 +55,15 @@ impl RepositoryRequestBody { } serde_json::from_slice(&body).map_err(RepositoryHandlerError::from) } + #[cfg(debug_assertions)] + #[instrument] + pub async fn body_as_json Deserialize<'a>>( + self, + ) -> Result { + let body = self.body_as_string().await?; + debug!(?body, "Body as JSON"); + Ok(serde_json::from_str(&body).map_err(BadRequestErrors::from)?) + } #[instrument] pub async fn body_as_string(self) -> Result { let body = self.body_as_bytes().await?; @@ -69,7 +79,17 @@ pub struct RepositoryRequest { pub path: StoragePath, pub authentication: RepositoryAuthentication, } - +impl RepositoryRequest { + pub fn user_agent_as_string(&self) -> Result, BadRequestErrors> { + let Some(header_value) = self.parts.headers.get(USER_AGENT) else { + return Ok(None); + }; + header_value + .to_str() + .map(Some) + .map_err(BadRequestErrors::from) + } +} impl AsRef for RepositoryRequest { fn as_ref(&self) -> &Parts { &self.parts @@ -144,10 +164,8 @@ fn response_file(meta: StorageFileMeta, content: StorageFileReader) -> Response< #[derive(Debug, From)] pub enum RepoResponse { FileResponse(StorageFile), - /// Should only be used for HEAD requests FileMetaResponse(StorageFileMeta), - Json(Value, StatusCode), - Generic(axum::response::Response), + Other(axum::response::Response), } impl RepoResponse { /// Default Response Format @@ -187,16 +205,7 @@ impl RepoResponse { .body(Body::empty()) .unwrap() } - Self::Json(json, status) => { - let body = serde_json::to_string(&json).unwrap(); - Response::builder() - .status(status) - .header(CONTENT_LENGTH, body.len()) - .header(CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) - .body(Body::from(body)) - .unwrap() - } - Self::Generic(response) => response, + Self::Other(response) => response, } } pub fn put_response(was_created: bool, location: impl AsRef) -> Self { @@ -301,7 +310,7 @@ impl RepoResponse { impl From> for RepoResponse { fn from(result: Result) -> Self { match result { - Ok(response) => RepoResponse::Generic(response), + Ok(response) => RepoResponse::Other(response), Err(err) => { error!(?err, "Failed to create response"); RepoResponse::internal_error(err) @@ -326,7 +335,6 @@ impl From> for RepoResponse { } } } - #[allow(dead_code)] #[derive(Debug, Clone, Deserialize)] pub struct RepoRequestPath { diff --git a/nitro_repo/src/repository/utils.rs b/nitro_repo/src/repository/utils.rs index 08101516..2ed13f35 100644 --- a/nitro_repo/src/repository/utils.rs +++ b/nitro_repo/src/repository/utils.rs @@ -1,4 +1,5 @@ use nr_core::{ + database::project::{DBProject, DBProjectVersion, ProjectDBType}, repository::Visibility, user::permissions::{HasPermissions, RepositoryActions}, }; @@ -7,6 +8,8 @@ use uuid::Uuid; use crate::app::authentication::Authentication; +use super::{Repository, RepositoryHandlerError}; + pub async fn can_read_repository( auth: Option, visibility: Visibility, @@ -20,3 +23,23 @@ pub async fn can_read_repository( .await?), } } +pub trait RepositoryExt: Repository { + async fn get_project_from_key( + &self, + project_key: &str, + ) -> Result, RepositoryHandlerError> { + let project = + DBProject::find_by_project_key(project_key, self.id(), &self.site().as_ref()).await?; + Ok(project) + } + async fn get_project_version( + &self, + project: Uuid, + version: &str, + ) -> Result, RepositoryHandlerError> { + let version = + DBProjectVersion::find_by_version_and_project(version, project, &self.site().database) + .await?; + Ok(version) + } +} diff --git a/site/package-lock.json b/site/package-lock.json index fd4b4e36..d162d4c4 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -64,7 +64,7 @@ "storybook": "^8.2.9", "typescript": "~5.5.4", "vite": "^5.4.2", - "vite-plugin-vue-devtools": "^7.3.8", + "vite-plugin-vue-devtools": "^7.3.9", "vue-tsc": "^2.0.29" } }, @@ -3152,9 +3152,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", - "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], @@ -3166,9 +3166,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", - "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], @@ -3180,9 +3180,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", - "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], @@ -3194,9 +3194,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", - "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], @@ -3208,9 +3208,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", - "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "cpu": [ "arm" ], @@ -3222,9 +3222,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", - "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", "cpu": [ "arm" ], @@ -3236,9 +3236,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", - "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], @@ -3250,9 +3250,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", - "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], @@ -3264,9 +3264,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", - "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", "cpu": [ "ppc64" ], @@ -3278,9 +3278,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", - "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], @@ -3292,9 +3292,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", - "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", "cpu": [ "s390x" ], @@ -3306,9 +3306,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", - "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], @@ -3320,9 +3320,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", - "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], @@ -3334,9 +3334,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", - "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], @@ -3348,9 +3348,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", - "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], @@ -3362,9 +3362,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", - "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], @@ -3886,9 +3886,9 @@ } }, "node_modules/@storybook/core/node_modules/@types/node": { - "version": "18.19.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.45.tgz", - "integrity": "sha512-VZxPKNNhjKmaC1SUYowuXSRSMGyQGmQjvvA1xE4QZ0xce2kLtEhPDS+kqpCPBZYgqblCLQ2DAjSzmgCM5auvhA==", + "version": "18.19.46", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.46.tgz", + "integrity": "sha512-vnRgMS7W6cKa1/0G3/DTtQYpVrZ8c0Xm6UkLaVFrb9jtcVC3okokW09Ki1Qdrj9ISokszD69nY4WDLRlvHlhAA==", "dev": true, "license": "MIT", "dependencies": { @@ -5082,14 +5082,14 @@ "license": "MIT" }, "node_modules/@vue/devtools-core": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.3.8.tgz", - "integrity": "sha512-mEwsR7GMklWuPOBH/++DiJe0GWqQ0syDtWP0HhU8m9tebs5zQtujMXrgu+cgBAKquJAWnBz0PwNzBgBD2P+M9A==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.3.9.tgz", + "integrity": "sha512-B5zAl9ulNjI6nknSnGNRzmP/ldR9ADUwwT8HkI8Hejo1W00uK9ABUahbfrXzME296rBfmwhQuCFwJ6t9KFdbXQ==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.3.8", - "@vue/devtools-shared": "^7.3.8", + "@vue/devtools-kit": "^7.3.9", + "@vue/devtools-shared": "^7.3.9", "mitt": "^3.0.1", "nanoid": "^3.3.4", "pathe": "^1.1.2", @@ -5119,13 +5119,13 @@ } }, "node_modules/@vue/devtools-kit": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.8.tgz", - "integrity": "sha512-HYy3MQP1nZ6GbE4vrgJ/UB+MvZnhYmEwCa/UafrEpdpwa+jNCkz1ZdUrC5I7LpkH1ShREEV2/pZlAQdBj+ncLQ==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.9.tgz", + "integrity": "sha512-Gr17nA+DaQzqyhNx1DUJr1CJRzTRfbIuuC80ZgU8MD/qNO302tv9la+ROi+Uaw+ULVwU9T71GnwLy4n8m9Lspg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.3.8", + "@vue/devtools-shared": "^7.3.9", "birpc": "^0.2.17", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -5135,9 +5135,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.8.tgz", - "integrity": "sha512-1NiJbn7Yp47nPDWhFZyEKpB2+5/+7JYv8IQnU0ccMrgslPR2dL7u1DIyI7mLqy4HN1ll36gQy0k8GqBYSFgZJw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.9.tgz", + "integrity": "sha512-CdfMRZKXyI8vw+hqOcQIiLihB6Hbbi7WNZGp7LsuH1Qe4aYAFmTaKjSciRZ301oTnwmU/knC/s5OGuV6UNiNoA==", "dev": true, "license": "MIT", "dependencies": { @@ -6050,9 +6050,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001651", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", - "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "version": "1.0.30001653", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz", + "integrity": "sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw==", "dev": true, "funding": [ { @@ -10341,9 +10341,9 @@ } }, "node_modules/nypm": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", - "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.11.tgz", + "integrity": "sha512-E5GqaAYSnbb6n1qZyik2wjPDZON43FqOJO59+3OkWrnmQtjggrMOVnsyzfjxp/tS6nlYJBA4zRA5jSM2YaadMg==", "dev": true, "license": "MIT", "dependencies": { @@ -10351,8 +10351,8 @@ "consola": "^3.2.3", "execa": "^8.0.1", "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" }, "bin": { "nypm": "dist/cli.mjs" @@ -11367,9 +11367,9 @@ } }, "node_modules/prosemirror-view": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.0.tgz", - "integrity": "sha512-oBFfImMwV9siXRW20hJ/pASfYz7dcHqBuRR8xtF4/Hot9dGwfXMjTgTQKhTFyr+iF0Wn4EFGXACab5dywNOaYQ==", + "version": "1.34.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.34.1.tgz", + "integrity": "sha512-KS2xmqrAM09h3SLu1S2pNO/ZoIP38qkTJ6KFd7+BeSfmX/ek0n5yOfGuiTZjFNTC8GOsEIUa1tHxt+2FMu3yWQ==", "license": "MIT", "dependencies": { "prosemirror-model": "^1.20.0", @@ -12004,9 +12004,9 @@ } }, "node_modules/rollup": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", - "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, "license": "MIT", "dependencies": { @@ -12020,22 +12020,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.0", - "@rollup/rollup-android-arm64": "4.21.0", - "@rollup/rollup-darwin-arm64": "4.21.0", - "@rollup/rollup-darwin-x64": "4.21.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", - "@rollup/rollup-linux-arm-musleabihf": "4.21.0", - "@rollup/rollup-linux-arm64-gnu": "4.21.0", - "@rollup/rollup-linux-arm64-musl": "4.21.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", - "@rollup/rollup-linux-riscv64-gnu": "4.21.0", - "@rollup/rollup-linux-s390x-gnu": "4.21.0", - "@rollup/rollup-linux-x64-gnu": "4.21.0", - "@rollup/rollup-linux-x64-musl": "4.21.0", - "@rollup/rollup-win32-arm64-msvc": "4.21.0", - "@rollup/rollup-win32-ia32-msvc": "4.21.0", - "@rollup/rollup-win32-x64-msvc": "4.21.0", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, @@ -13334,13 +13334,12 @@ } }, "node_modules/vfile": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", - "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", "license": "MIT", "dependencies": { "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" }, "funding": { @@ -13468,15 +13467,15 @@ } }, "node_modules/vite-plugin-vue-devtools": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.3.8.tgz", - "integrity": "sha512-b5t4wxCb5g5cjh+odNpgnB7iX7gA6FJnKugFqX2/YZX9I4fvMjlj1bUnCKnvPlmwnFxClYgdmgZcCh2RyhZgvw==", + "version": "7.3.9", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.3.9.tgz", + "integrity": "sha512-ybDV2kepW0NpusvtfbRKHs0pvyrReNcFtL572gyZ6Alox6u5uebYefd2eAG/7mJSU3NPI5UxUH1e/Mof5exdlw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-core": "^7.3.8", - "@vue/devtools-kit": "^7.3.8", - "@vue/devtools-shared": "^7.3.8", + "@vue/devtools-core": "^7.3.9", + "@vue/devtools-kit": "^7.3.9", + "@vue/devtools-shared": "^7.3.9", "execa": "^8.0.1", "sirv": "^2.0.4", "vite-plugin-inspect": "^0.8.5", diff --git a/site/package.json b/site/package.json index 31bb647a..5de86b56 100644 --- a/site/package.json +++ b/site/package.json @@ -71,7 +71,7 @@ "storybook": "^8.2.9", "typescript": "~5.5.4", "vite": "^5.4.2", - "vite-plugin-vue-devtools": "^7.3.8", + "vite-plugin-vue-devtools": "^7.3.9", "vue-tsc": "^2.0.29" } } diff --git a/site/src/components/repository/types/npm/NPMConfig.vue b/site/src/components/repository/types/npm/NPMConfig.vue new file mode 100644 index 00000000..1847cdd9 --- /dev/null +++ b/site/src/components/repository/types/npm/NPMConfig.vue @@ -0,0 +1,98 @@ + + diff --git a/site/src/components/repository/types/npm/NPMProjectHelper.vue b/site/src/components/repository/types/npm/NPMProjectHelper.vue new file mode 100644 index 00000000..4ae8958a --- /dev/null +++ b/site/src/components/repository/types/npm/NPMProjectHelper.vue @@ -0,0 +1,25 @@ + + diff --git a/site/src/components/repository/types/npm/npm.ts b/site/src/components/repository/types/npm/npm.ts index e69de29b..06807f38 100644 --- a/site/src/components/repository/types/npm/npm.ts +++ b/site/src/components/repository/types/npm/npm.ts @@ -0,0 +1,38 @@ +import { NpmIcon } from 'vue3-simple-icons' +import NPMProjectHelper from './NPMProjectHelper.vue' +export const MavenFrontendDefinition = { + name: 'npm', + properName: 'npm', + projectComponent: { + component: NPMProjectHelper, + props: {} + }, + icons: [ + { + name: 'NPM', + component: NpmIcon, + url: 'https://www.npmjs.com/', + props: {} + } + ] +} +export interface MavenProxyRoute { + url: string + name?: string +} +export interface NPMProxyConfigType { + routes: MavenProxyRoute[] +} +export function defaultProxy(): NPMProxyConfigType { + return { + routes: [] + } +} +export type NPMConfigType = + | { + type: 'Hosted' + } + | { + type: 'Proxy' + config: NPMProxyConfigType + } diff --git a/site/src/types/repository.ts b/site/src/types/repository.ts index 3f8cde5a..32ff4fc8 100644 --- a/site/src/types/repository.ts +++ b/site/src/types/repository.ts @@ -6,6 +6,7 @@ import RepositoryPageEditor from '@/components/admin/repository/configs/Reposito import { apiURL } from '@/config' import { MavenFrontendDefinition } from '@/components/repository/types/maven/maven' +import NPMConfig from '@/components/repository/types/npm/NPMConfig.vue' export interface RepositoryTypeDescription { type_name: string @@ -52,6 +53,11 @@ export const configTypes: ConfigType[] = [ name: 'page', title: 'Page', component: RepositoryPageEditor + }, + { + name: 'npm', + title: 'NPM', + component: NPMConfig } ] export interface RepositoryIconDef {