diff --git a/.dockerignore b/.dockerignore index 68b92ba1..4ecd2491 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,7 @@ target/ .vscode/ test/ .env +site/node_modules/ +docs/node_modules/ +nr_tests.env +storage_tests \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3b260e7b..75ed347a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -641,6 +641,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" + [[package]] name = "cc" version = "1.2.9" @@ -1108,19 +1114,21 @@ dependencies = [ [[package]] name = "digestible" -version = "0.2.2" -source = "git+https://github.com/wyatt-herkamp/digestible.git#e1a5bbcf35d23010c6776b60a81e2aac38507360" +version = "0.2.3" +source = "git+https://github.com/wyatt-herkamp/digestible.git#2090b4e84edfd3a7ddde6854faaaf10edd9ab80d" dependencies = [ "base64 0.22.1", "byteorder", + "chrono", "digest", "digestible-macros", + "uuid", ] [[package]] name = "digestible-macros" -version = "0.2.2" -source = "git+https://github.com/wyatt-herkamp/digestible.git#e1a5bbcf35d23010c6776b60a81e2aac38507360" +version = "0.2.3" +source = "git+https://github.com/wyatt-herkamp/digestible.git#2090b4e84edfd3a7ddde6854faaaf10edd9ab80d" dependencies = [ "proc-macro2", "quote", @@ -2479,6 +2487,7 @@ dependencies = [ "badge-maker", "base64 0.22.1", "bytes", + "camino", "chrono", "clap", "current_semver", @@ -2496,6 +2505,7 @@ dependencies = [ "lettre", "maven-rs", "mime", + "mime_guess", "nr-core", "nr-macros", "nr-storage", @@ -2538,6 +2548,7 @@ dependencies = [ "utoipa", "utoipa-scalar", "uuid", + "walkdir", "zip", ] @@ -4878,9 +4889,9 @@ dependencies = [ [[package]] name = "utoipa-scalar" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088e93bf19f6bd06e0aacb02ca432b3c5a449c4aec2e4aa9fc333a667f2b2c55" +checksum = "59559e1509172f6b26c1cdbc7247c4ddd1ac6560fe94b584f81ee489b141f719" dependencies = [ "axum 0.8.1", "serde", diff --git a/Cargo.toml b/Cargo.toml index e27518bb..cd458c75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,8 @@ url = "2" ## Hashing digestible = { git = "https://github.com/wyatt-herkamp/digestible.git", features = [ "base64", + "uuid", + "chrono", ] } digest = { version = "0.10", features = ["std", "alloc"] } md-5 = "0.10" diff --git a/Dockerfile b/Dockerfile index 3b183f79..b3f1f5ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,18 +3,18 @@ COPY . /home/build WORKDIR /home/build RUN apt-get update; apt-get install -y curl \ - && curl -sL https://deb.nodesource.com/setup_20.x | bash - \ + && curl -sL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y nodejs \ && curl -L https://www.npmjs.com/install.sh | sh RUN apt-get install -y libssl-dev pkg-config -WORKDIR /home/build/backend -RUN cargo build --release # Build Frontend -WORKDIR /home/build/frontend +WORKDIR /home/build/site RUN npm install RUN npm run build +WORKDIR /home/build/backend +RUN cargo build --release --features frontend LABEL org.label-schema.name="nitro_repo" \ org.label-schema.vendor="wyatt-herkamp" \ @@ -23,13 +23,13 @@ LABEL org.label-schema.name="nitro_repo" \ org.label-schema.description="An open source artifact manager. Written in Rust back end and an Vue front end to create a fast and modern experience" # The Final Image -FROM rust:slim-bookworm +FROM debian:bookworm-slim -RUN apt-get install libssl1.1 +RUN apt-get update -y && apt-get -y install libssl-dev openssl RUN mkdir -p /opt/nitro-repo RUN mkdir -p /app -COPY --from=build /home/build/target/release/nitro-repo /app/nitro-repo +COPY --from=build /home/build/target/release/nitro_repo /app/nitro-repo COPY --from=build /home/build/entrypoint.sh /app/entrypoint.sh WORKDIR /opt/nitro-repo ENTRYPOINT ["/bin/sh", "entrypoint.sh"] diff --git a/crates/core/src/storage/storage_path.rs b/crates/core/src/storage/storage_path.rs index d1e7c426..447ef963 100644 --- a/crates/core/src/storage/storage_path.rs +++ b/crates/core/src/storage/storage_path.rs @@ -2,10 +2,8 @@ use std::{fmt::Display, path::PathBuf}; use http::Uri; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_json::map::IntoIter; use thiserror::Error; use tracing::instrument; -use utoipa::ToSchema; #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct StoragePathComponent(String); @@ -59,7 +57,7 @@ impl AsRef for StoragePathComponent { } } -/// A Storage path is a UTF-8 only path. Where the root is the base of the storage. +/// A Storage path is a UTF-8 only path. Where the root is the base of the repository. #[derive(Debug, Clone, Hash, PartialEq, Eq, Default)] pub struct StoragePath { components: Vec, @@ -67,7 +65,7 @@ pub struct StoragePath { } impl utoipa::__dev::ComposeSchema for StoragePath { fn compose( - mut generics: Vec>, + _: Vec>, ) -> utoipa::openapi::RefOr { utoipa::openapi::ObjectBuilder::new() .schema_type(utoipa::openapi::schema::SchemaType::new( @@ -129,7 +127,6 @@ impl StoragePath { pub fn is_directory(&self) -> bool { self.trailing_slash == Some(true) } - } impl From> for StoragePath { fn from(value: Vec) -> Self { diff --git a/crates/storage/src/dyn_storage.rs b/crates/storage/src/dyn_storage.rs index 30b06f24..0179015b 100644 --- a/crates/storage/src/dyn_storage.rs +++ b/crates/storage/src/dyn_storage.rs @@ -1,5 +1,4 @@ use nr_core::storage::StoragePath; -use serde_json::Value; use uuid::Uuid; use crate::{ diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index 61875b7b..a92bbe18 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -1,9 +1,8 @@ -use std::time::SystemTimeError; use thiserror::Error; use crate::{ - local::{error::LocalStorageError, LocalStorage}, + local::error::LocalStorageError, s3::S3StorageError, InvalidConfigType, PathCollisionError, }; diff --git a/crates/storage/src/fs/file.rs b/crates/storage/src/fs/file.rs index 48189dee..7e433fce 100644 --- a/crates/storage/src/fs/file.rs +++ b/crates/storage/src/fs/file.rs @@ -3,9 +3,9 @@ use std::{fmt::Debug, fs::File, io, path::Path}; use crate::{is_hidden_file, local::error::LocalStorageError, FileMeta}; use super::{ - utils::MetadataUtils, FileContent, FileContentBytes, FileHashes, SerdeMime, StorageFileReader, + FileHashes, SerdeMime, StorageFileReader, }; -use chrono::{DateTime, FixedOffset, Local}; +use chrono::{DateTime, FixedOffset}; use derive_more::derive::From; use nr_core::storage::FileTypeCheck; diff --git a/crates/storage/src/fs/file_meta.rs b/crates/storage/src/fs/file_meta.rs index 132c8ec7..49e3fc3e 100644 --- a/crates/storage/src/fs/file_meta.rs +++ b/crates/storage/src/fs/file_meta.rs @@ -13,7 +13,7 @@ use utoipa::ToSchema; use crate::{ fs::utils::MetadataUtils, - local::{error::LocalStorageError, LocalStorage}, + local::error::LocalStorageError, meta::RepositoryMeta, path::PathUtils, }; diff --git a/crates/storage/src/fs/path.rs b/crates/storage/src/fs/path.rs index 198c79d6..7967af72 100644 --- a/crates/storage/src/fs/path.rs +++ b/crates/storage/src/fs/path.rs @@ -14,6 +14,8 @@ pub enum ExtensionError { } pub trait PathUtils { + /// Gets the parent directory of the path or returns an error if it does not exist. + #[allow(unused)] fn parent_or_err(&self) -> Result<&Path, ParentDirectoryDoesNotExist>; /// Appends an extension to the path. fn add_extension(&self, extension: &str) -> Result; diff --git a/crates/storage/src/fs/utils.rs b/crates/storage/src/fs/utils.rs index 823f86b9..e8dfe6e7 100644 --- a/crates/storage/src/fs/utils.rs +++ b/crates/storage/src/fs/utils.rs @@ -1,7 +1,7 @@ use std::{ fs::File, io, - path::{Path, PathBuf}, + path::PathBuf, }; use chrono::{offset::LocalResult, DateTime, FixedOffset, Local, TimeZone}; diff --git a/crates/storage/src/local/mod.rs b/crates/storage/src/local/mod.rs index 4a9e2f3c..45c4b5d0 100644 --- a/crates/storage/src/local/mod.rs +++ b/crates/storage/src/local/mod.rs @@ -66,6 +66,7 @@ impl LocalStorageInner { /// ## Ok /// - First value is the path to the file /// - Second is the directory that the file is in + #[instrument(level = "debug")] pub fn get_path_for_creation( &self, repository: Uuid, @@ -77,12 +78,16 @@ impl LocalStorageInner { let mut iter = location.clone().into_iter().peekable(); while let Some(part) = iter.next() { if iter.peek().is_none() { + debug!(?part, "Last Part of Path"); parent_directory = path.clone(); } path = path.join(part.as_ref()); conflicting_path.push_mut(part.as_ref()); + trace!(?path, ?conflicting_path, "Checking Path"); if path.exists() { + trace!(?path, "Path Exists"); if path.is_file() { + warn!(?path, "Path is a file"); return Err(PathCollisionError { path: location.clone(), conflicts_with: conflicting_path, @@ -159,7 +164,7 @@ impl Storage for LocalStorage { config: BorrowedStorageTypeConfig::Local(&self.config), } } - #[instrument(name = "local_storage_save")] + #[instrument(name = "Storage::save_file", fields(storage_type = "local"))] async fn save_file( &self, repository: Uuid, @@ -186,7 +191,7 @@ impl Storage for LocalStorage { } Ok((bytes_written, new_file)) } - #[instrument(name = "local_storage_delete")] + #[instrument(name = "Storage::delete_file", fields(storage_type = "local"))] async fn delete_file( &self, repository: Uuid, @@ -206,7 +211,7 @@ impl Storage for LocalStorage { } Ok(true) } - #[instrument(name = "local_storage_get_info")] + #[instrument(name = "Storage::get_file_information", fields(storage_type = "local"))] async fn get_file_information( &self, repository: Uuid, @@ -221,7 +226,7 @@ impl Storage for LocalStorage { let meta = StorageFileMeta::read_from_path(path)?; Ok(Some(meta)) } - #[instrument(name = "local_storage_open_file")] + #[instrument(name = "Storage::open_file", fields(storage_type = "local"))] async fn open_file( &self, repository: Uuid, @@ -239,12 +244,16 @@ impl Storage for LocalStorage { }; Ok(Some(file)) } + #[instrument(name = "Storage::unload", fields(storage_type = "local"))] async fn unload(&self) -> Result<(), LocalStorageError> { info!(?self, "Unloading Local Storage"); // TODO: Implement Unload Ok(()) } - + #[instrument( + name = "Storage::validate_config_change", + fields(storage_type = "local") + )] async fn validate_config_change( &self, config: StorageTypeConfig, @@ -255,6 +264,7 @@ impl Storage for LocalStorage { } Ok(()) } + #[instrument(name = "Storage::get_repository_meta", fields(storage_type = "local"))] async fn get_repository_meta( &self, repository: Uuid, @@ -267,6 +277,7 @@ impl Storage for LocalStorage { let meta = FileMeta::get_or_create_local(&path)?; Ok(Some(meta.repository_meta)) } + #[instrument(name = "Storage::put_repository_meta", fields(storage_type = "local"))] async fn put_repository_meta( &self, repository: Uuid, @@ -285,6 +296,7 @@ impl Storage for LocalStorage { FileMeta::set_repository_meta(path, value)?; Ok(()) } + #[instrument(name = "Storage::file_exists", fields(storage_type = "local"))] async fn file_exists( &self, repository: Uuid, diff --git a/crates/storage/src/s3/mod.rs b/crates/storage/src/s3/mod.rs index 2f2fdbac..ecefd505 100644 --- a/crates/storage/src/s3/mod.rs +++ b/crates/storage/src/s3/mod.rs @@ -1,9 +1,9 @@ -use std::{borrow::Cow, io::SeekFrom, ops::Deref, path, str::FromStr, sync::Arc}; +use std::{borrow::Cow, ops::Deref, str::FromStr, sync::Arc}; use chrono::Local; use futures::future::BoxFuture; use mime::Mime; -use nr_core::storage::{self, StoragePath}; +use nr_core::storage::StoragePath; use regions::{CustomRegion, S3StorageRegion}; use s3::{ creds::{Credentials, Rfc3339OffsetDateTime}, @@ -48,7 +48,7 @@ impl S3StorageError { } } use crate::{ - error, meta::RepositoryMeta, utils::new_type_arc_type, BorrowedStorageConfig, + meta::RepositoryMeta, utils::new_type_arc_type, BorrowedStorageConfig, BorrowedStorageTypeConfig, DirectoryFileType, DynStorage, FileContent, FileContentBytes, FileFileType, FileHashes, FileType, InvalidConfigType, PathCollisionError, SerdeMime, StaticStorageFactory, Storage, StorageConfig, StorageConfigInner, StorageError, StorageFactory, @@ -302,7 +302,7 @@ impl S3StorageInner { Ok(Some(meta)) } - + #[instrument] async fn get_object_tagging(&self, path: &str) -> Result>, S3StorageError> { let (tags, status_code) = self.bucket.get_object_tagging(path).await?; if status_code == 404 { @@ -317,11 +317,7 @@ impl S3StorageInner { Ok(Some(tags)) } - async fn is_path_a_directory(&self, path: &str) -> Result { - let tags = self.get_object_tagging(path).await?; - Ok(false) - } async fn get_meta_tags(&self, path: &str) -> Result, S3StorageError> { let Some(tags) = self.get_object_tagging(path).await? else { return Ok(None); diff --git a/crates/storage/src/s3/regions.rs b/crates/storage/src/s3/regions.rs index 7f4fb865..374111ec 100644 --- a/crates/storage/src/s3/regions.rs +++ b/crates/storage/src/s3/regions.rs @@ -1,8 +1,9 @@ use s3::Region; use serde::{Deserialize, Serialize}; +use strum::EnumIter; use utoipa::ToSchema; -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ToSchema, EnumIter)] pub enum S3StorageRegion { /// us-east-1 UsEast1, diff --git a/crates/storage/src/testing.rs b/crates/storage/src/testing.rs index 7ebe53e4..1f8ef53d 100644 --- a/crates/storage/src/testing.rs +++ b/crates/storage/src/testing.rs @@ -1,6 +1,6 @@ use std::{env::current_dir, path::PathBuf}; -use nr_core::{repository::config, testing::logging::TestingLoggerConfig}; +use nr_core::testing::logging::TestingLoggerConfig; use serde::{Deserialize, Serialize}; use tracing::info; use uuid::Uuid; @@ -8,8 +8,7 @@ pub mod tests; use crate::{ local::{LocalConfig, LocalStorage, LocalStorageFactory}, s3::{regions::CustomRegion, S3Config, S3Credentials, S3StorageFactory}, - StaticStorageFactory, StorageConfig, StorageConfigInner, StorageError, StorageFactory, - StorageTypeConfig, + StaticStorageFactory, StorageConfig, StorageConfigInner, StorageTypeConfig, }; pub mod storage; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/storage/src/testing/storage.rs b/crates/storage/src/testing/storage.rs index c2d88ec0..4e22cc59 100644 --- a/crates/storage/src/testing/storage.rs +++ b/crates/storage/src/testing/storage.rs @@ -1,9 +1,8 @@ use std::sync::Arc; -use ahash::{HashSet, HashSetExt}; +use ahash::HashSet; use nr_core::storage::StoragePath; use tokio::sync::Mutex; -use tracing::instrument; use uuid::Uuid; use crate::{meta::RepositoryMeta, FileType, Storage}; @@ -15,7 +14,6 @@ pub struct CreatedFiles { #[derive(Debug, Clone, Default)] pub struct TestingInternalStorage { pub created_files: HashSet, - pub created_meta_files: HashSet, } impl TestingInternalStorage { pub fn add_created_file(&mut self, repository: Uuid, path: StoragePath) -> bool { @@ -25,14 +23,6 @@ impl TestingInternalStorage { self.created_files .remove(&CreatedFiles { repository, path }) } - pub fn add_created_meta_file(&mut self, repository: Uuid, path: StoragePath) -> bool { - self.created_meta_files - .insert(CreatedFiles { repository, path }) - } - pub fn remove_created_meta_file(&mut self, repository: Uuid, path: StoragePath) -> bool { - self.created_meta_files - .remove(&CreatedFiles { repository, path }) - } } pub struct TestingStorage { storage: ST, @@ -116,17 +106,17 @@ impl Storage for TestingStorage { async fn put_repository_meta( &self, - repository: Uuid, - location: &StoragePath, - value: RepositoryMeta, + _repository: Uuid, + _location: &StoragePath, + _value: RepositoryMeta, ) -> Result<(), Self::Error> { todo!() } async fn get_repository_meta( &self, - repository: Uuid, - location: &StoragePath, + _repository: Uuid, + _location: &StoragePath, ) -> Result, Self::Error> { todo!() } diff --git a/crates/storage/src/testing/tests.rs b/crates/storage/src/testing/tests.rs index 0c008e2c..dc80206f 100644 --- a/crates/storage/src/testing/tests.rs +++ b/crates/storage/src/testing/tests.rs @@ -1,13 +1,11 @@ -use std::path; use nr_core::storage::StoragePath; -use tokio::io::AsyncReadExt; use tracing::{debug, info}; use uuid::Uuid; -use crate::{FileContent, FileType, Storage, StorageError, StorageFile}; +use crate::{FileContent, Storage, StorageError, StorageFile}; -use super::{storage::TestingStorage, TestingStorageType}; +use super::storage::TestingStorage; pub async fn full_test(storage: TestingStorage) -> anyhow::Result<()> { write_then_read(&storage).await?; write_multiple_then_list(&storage).await?; diff --git a/entrypoint.sh b/entrypoint.sh index 134c6185..7d549115 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,3 +1,3 @@ #!/bin/bash -exec /app/nitro-repo --config /opt/thd-helper/thd-helper.toml start +exec /app/nitro-repo start --config /opt/thd-helper/thd-helper.toml diff --git a/justfile b/justfile index 64b139ab..d38f4a23 100644 --- a/justfile +++ b/justfile @@ -2,4 +2,9 @@ build-release: cargo build --release fmt: cargo fmt --all - cd site && npm run format \ No newline at end of file + cd site && npm run format + + +release-dev-docker: + docker build -t git.kingtux.dev/wherkamp/nitro_repo/nitro_repo:latest . + docker push git.kingtux.dev/wherkamp/nitro_repo/nitro_repo:latest \ No newline at end of file diff --git a/nitro_repo/Cargo.toml b/nitro_repo/Cargo.toml index 23a64fb8..83d80b35 100644 --- a/nitro_repo/Cargo.toml +++ b/nitro_repo/Cargo.toml @@ -23,9 +23,11 @@ hyper.workspace = true hyper-util.workspace = true http.workspace = true mime.workspace = true +mime_guess.workspace = true + http-body-util.workspace = true utoipa = { workspace = true, features = ["axum_extras"] } -utoipa-scalar = { version = "0.2", features = ["axum"], optional = true } +utoipa-scalar = { version = "0.3", features = ["axum"], optional = true } async-trait = "0.1" # TLS tokio-rustls = "0.26" @@ -116,7 +118,15 @@ url = "2" inquire = "0.7" [features] default = ["utoipa-scalar"] -builtin_frontend = [] +frontend = [] [lints] workspace = true + + +[build-dependencies] +zip = { version = "2.1" } + +walkdir = "2" +anyhow = "1.0" +camino = "1.1" diff --git a/nitro_repo/build.rs b/nitro_repo/build.rs index f328e4d9..1f5d2ef7 100644 --- a/nitro_repo/build.rs +++ b/nitro_repo/build.rs @@ -1 +1,99 @@ -fn main() {} +#![allow(dead_code)] +use std::{ + env, + fs::File, + io::{prelude::*, Seek, Write}, + iter::Iterator, + path::{Path, PathBuf}, +}; + +use anyhow::Context; +use walkdir::{DirEntry, WalkDir}; +use zip::{write::SimpleFileOptions, ZipWriter}; + +fn main() -> anyhow::Result<()> { + #[cfg(feature = "frontend")] + build_frontend()?; + Ok(()) +} +fn build_frontend() -> anyhow::Result<()> { + let manifest_dir = env::var("CARGO_MANIFEST_DIR") + .map(PathBuf::from) + .with_context(|| "CARGO_MANIFEST_DIR not set")? + .parent() + .context("Invalid CARGO_MANIFEST_DIR. (Could not get parent)")? + .to_path_buf(); + let frontend_src = manifest_dir.join("site"); + if !frontend_src.exists() { + return Err(anyhow::anyhow!("site directory not found")); + } + + println!("cargo::rerun-if-changed={}", manifest_dir.display()); + zip_site(frontend_src)?; + Ok(()) +} + +/// Bundling files seem to be broken with Android. So as a work around. I will zip the files and include them in the binary. +fn zip_site(frontend: impl AsRef) -> anyhow::Result<()> { + let out_dir = env::var("OUT_DIR").with_context(|| "OUT_DIR not set")?; + let src = frontend.as_ref().join("dist"); + if !src.exists() { + return Err(anyhow::anyhow!("site build directory not found")); + } + let dst = PathBuf::from(out_dir).join("frontend.zip"); + if dst.exists() { + std::fs::remove_file(&dst)?; + } + let file = File::create(&dst)?; + + let walkdir = WalkDir::new(&src); + let it = walkdir.into_iter(); + + internal_zip_dir( + &mut it.filter_map(|e| e.ok()), + &src, + file, + zip::CompressionMethod::Stored, + )?; + println!("cargo:rustc-env=FRONTEND_ZIP={}", dst.display()); + println!("cargo:rustc-env=FRONTEND_SRC={}", src.display()); + + Ok(()) +} +fn internal_zip_dir( + it: &mut dyn Iterator, + prefix: &Path, + writer: T, + method: zip::CompressionMethod, +) -> anyhow::Result<()> +where + T: Write + Seek, +{ + let mut zip = ZipWriter::new(writer); + let options = SimpleFileOptions::default() + .compression_method(method) + .unix_permissions(0o755); + + let mut buffer = Vec::with_capacity(1024); + for entry in it { + let absolute_path = entry.path(); + let stripped_path = entry.path().strip_prefix(prefix)?; + let name = camino::Utf8Path::from_path(stripped_path) + .with_context(|| format!("{stripped_path:?} Could not be converted to UTF-8"))?; + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if absolute_path.is_file() { + zip.start_file(name.as_str(), options)?; + let mut f = File::open(absolute_path)?; + + f.read_to_end(&mut buffer)?; + zip.write_all(&buffer)?; + buffer.clear(); + } else if !name.as_str().is_empty() { + zip.add_directory(name.to_string(), options)?; + } + } + zip.finish()?; + Result::Ok(()) +} diff --git a/nitro_repo/src/app/api/storage.rs b/nitro_repo/src/app/api/storage.rs index 2c207df1..f344ecfc 100644 --- a/nitro_repo/src/app/api/storage.rs +++ b/nitro_repo/src/app/api/storage.rs @@ -1,10 +1,9 @@ use axum::{ - body::Body, extract::{Path, Query, State}, response::{IntoResponse, Response}, + routing::{get, post}, Json, }; -use http::{header::CONTENT_TYPE, StatusCode}; use nr_core::{ database::storage::{DBStorage, DBStorageNoConfig, NewDBStorage, StorageDBType}, storage::StorageName, @@ -13,46 +12,61 @@ use nr_core::{ use nr_storage::{local::LocalConfig, StorageConfig, StorageTypeConfig}; use serde::{Deserialize, Serialize}; use tracing::{error, instrument}; -use utoipa::{OpenApi, ToSchema}; +use utoipa::{IntoParams, OpenApi, ToSchema}; use uuid::Uuid; - +mod local; +mod s3; use crate::{ app::{ authentication::Authentication, responses::{ InvalidStorageConfig, InvalidStorageType, MissingPermission, ResponseBuilderExt, }, - Instance, NitroRepo, + NitroRepo, }, error::InternalError, + utils::{response_builder::ResponseBuilder, responses::ConflictResponse}, }; #[derive(OpenApi)] #[openapi( - paths(list_storages, new_storage, get_storage, local_storage_path_helper), - components(schemas(DBStorage, NewStorageRequest, StorageTypeConfig, LocalConfig)) + paths(list_storages, new_storage, get_storage), + components(schemas(DBStorage, NewStorageRequest, StorageTypeConfig, LocalConfig)), + nest( + (path = "/local", api = local::LocalStorageAPI, tags=["local", "storage"]), + (path = "/s3", api = s3::S3StorageAPI, tags=["s3", "storage"]) + ), + tags( + (name= "local", description = "Local Storage"), + (name= "s3", description = "S3 Storage"), + ) )] pub struct StorageAPI; pub fn storage_routes() -> axum::Router { axum::Router::new() - .route("/list", axum::routing::get(list_storages)) - .route("/new/{storage_type}", axum::routing::post(new_storage)) - .route("/{id}", axum::routing::get(get_storage)) - .route( - "/local-storage-path-helper", - axum::routing::post(local_storage_path_helper), - ) + .route("/list", get(list_storages)) + .route("/new/{storage_type}", post(new_storage)) + .route("/{id}", get(get_storage)) + .nest("/local", local::local_storage_routes()) + .nest("/s3", s3::s3_storage_api()) } -#[derive(Debug, Default, Serialize, Deserialize, ToSchema)] +#[derive(Debug, Default, Serialize, Deserialize, ToSchema, IntoParams)] #[serde(default)] +#[into_params(parameter_in = Query)] pub struct StorageListRequest { + /// Include the storage configuration in the response (default: false) pub include_config: bool, + /// Only include active storages (default: false) pub active_only: bool, } #[utoipa::path( get, path = "/list", + params( + StorageListRequest, + ), responses( (status = 200, description = "All Storages registered to the system.", body = [DBStorage]), + (status = 200, description = "All the storages without the configs", body = [DBStorageNoConfig]), (status = 403, description = "Does not have permission to view storages") ) )] @@ -61,80 +75,17 @@ pub async fn list_storages( State(site): State, auth: Authentication, Query(request): Query, -) -> Result { - if auth.is_admin_or_system_manager() { - if request.include_config { - let storages = DBStorage::get_all(&site.database).await?; - Response::builder().status(200).json_body(&storages) - } else { - let storages = DBStorageNoConfig::get_all(&site.database).await?; - Response::builder().status(200).json_body(&storages) - } - } else { - Ok(MissingPermission::StorageManager.into_response()) - } -} -#[derive(Debug, Serialize, Deserialize, ToSchema)] -pub struct LocalStoragePathHelperRequest { - pub path: Option, -} -#[derive(Debug, Serialize, Deserialize, ToSchema)] -#[serde(tag = "type", content = "value")] -pub enum LocalStoragePathHelperResponse { - CurrentPath(String), - Directories(Vec), - PathDoesNotExist, -} -#[utoipa::path( - get, - path = "/local-storage-path-helper", - responses( - (status = 200, description = "information about the Site", body = Instance) - ) -)] -#[instrument] -pub async fn local_storage_path_helper( - auth: Authentication, - Json(request): Json, ) -> Result { if !auth.is_admin_or_system_manager() { return Ok(MissingPermission::StorageManager.into_response()); } - let path = request.path.unwrap_or_default().trim().to_owned(); - if path.is_empty() { - let working_dir = std::env::current_dir().unwrap(); - let current_path = working_dir.to_string_lossy().to_string(); - return Ok(Response::builder() - .status(200) - .header("Content-Type", "application/json") - .body(Body::from( - serde_json::to_string(&LocalStoragePathHelperResponse::CurrentPath(current_path)) - .unwrap(), - )) - .unwrap()); - } - let path = std::path::Path::new(&path); - let response = if path.exists() { - // List directories - let mut directories = vec![]; - for entry in std::fs::read_dir(path).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - if path.is_dir() { - if let Some(file_name) = path.file_name() { - directories.push(file_name.to_string_lossy().to_string()); - } - } - } - LocalStoragePathHelperResponse::Directories(directories) + if request.include_config { + let storages = DBStorage::get_all(&site.database).await?; + Response::builder().status(200).json_body(&storages) } else { - LocalStoragePathHelperResponse::PathDoesNotExist - }; - Ok(Response::builder() - .status(200) - .header("Content-Type", "application/json") - .body(Body::from(serde_json::to_string(&response).unwrap())) - .unwrap()) + let storages = DBStorageNoConfig::get_all(&site.database).await?; + Response::builder().status(200).json_body(&storages) + } } #[derive(Debug, Serialize, Deserialize, Clone, ToSchema)] @@ -149,7 +100,7 @@ pub struct NewStorageRequest { request_body = NewStorageRequest, responses( (status = 201, description = "Storage Successfully Created", body = DBStorage), - (status = 409, description = "Name already in use"), + ConflictResponse, (status = 400, description = "Invalid Storage Config"), ), params( @@ -167,10 +118,7 @@ pub async fn new_storage( return Ok(MissingPermission::StorageManager.into_response()); } if !DBStorage::is_name_available(&request.name, site.as_ref()).await? { - return Ok(Response::builder() - .status(StatusCode::CONFLICT) - .body("Name already in use".into()) - .unwrap()); + return Ok(ConflictResponse::from("name").into_response()); } let Some(storage_factory) = site.get_storage_factory(&storage_type) else { @@ -183,15 +131,13 @@ pub async fn new_storage( error!("Failed to test storage config: {}", error); return Ok(InvalidStorageConfig(error).into_response()); } + let config = serde_json::to_value(request.config).unwrap(); let storage = NewDBStorage::new(storage_type, request.name, config) .insert(&site.database) .await?; let Some(storage) = storage else { - return Ok(Response::builder() - .status(StatusCode::CONFLICT) - .body("Name already in use".into()) - .unwrap()); + return Ok(ConflictResponse::from("name").into_response()); }; let id = storage.id; let storage_config = match StorageConfig::try_from(storage.clone()) { @@ -210,11 +156,7 @@ pub async fn new_storage( return Err(InternalError::from(err)); } } - Ok(Response::builder() - .status(StatusCode::CREATED) - .header(CONTENT_TYPE, "application/json") - .body(Body::from(serde_json::to_string(&storage).unwrap())) - .unwrap()) + Ok(ResponseBuilder::created().json(&storage)) } #[utoipa::path( post, @@ -227,7 +169,6 @@ pub async fn new_storage( #[instrument] pub async fn get_storage( auth: Authentication, - Path(id): Path, State(site): State, ) -> Result { @@ -236,13 +177,7 @@ pub async fn get_storage( } let storage = DBStorage::get_by_id(id, &site.database).await?; match storage { - Some(storage) => { - let response = Json(storage).into_response(); - Ok(response) - } - None => Ok(Response::builder() - .status(404) - .body("Storage not found".into()) - .unwrap()), + Some(storage) => Ok(ResponseBuilder::ok().json(&storage)), + None => Ok(ResponseBuilder::not_found().body("Storage not found")), } } diff --git a/nitro_repo/src/app/api/storage/local.rs b/nitro_repo/src/app/api/storage/local.rs new file mode 100644 index 00000000..e29d58db --- /dev/null +++ b/nitro_repo/src/app/api/storage/local.rs @@ -0,0 +1,81 @@ +use axum::{ + response::{IntoResponse, Response}, + routing::post, + Json, +}; +use nr_core::user::permissions::HasPermissions; +use serde::{Deserialize, Serialize}; +use storage::NitroRepo; +use tracing::instrument; +use utoipa::{OpenApi, ToSchema}; + +use crate::{ + app::{api::storage, authentication::Authentication, responses::MissingPermission}, + error::InternalError, + utils::response_builder::ResponseBuilder, +}; +#[derive(OpenApi)] +#[openapi( + paths(path_helper), + components(schemas(LocalStoragePathHelperRequest, LocalStoragePathHelperResponse,)) +)] +pub struct LocalStorageAPI; +pub fn local_storage_routes() -> axum::Router { + axum::Router::new().route("/path-helper", post(path_helper)) +} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct LocalStoragePathHelperRequest { + pub path: Option, +} +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(tag = "type", content = "value")] +pub enum LocalStoragePathHelperResponse { + /// The current working directory + CurrentPath(String), + /// A list of directories in the path + Directories(Vec), + /// The path does not exist + PathDoesNotExist, +} +#[utoipa::path( + get, + path = "/local/path-helper", + responses( + (status = 200, description = "a path suggestion", body = LocalStoragePathHelperResponse) + ) +)] +#[instrument] +pub async fn path_helper( + auth: Authentication, + Json(request): Json, +) -> Result { + if !auth.is_admin_or_system_manager() { + return Ok(MissingPermission::StorageManager.into_response()); + } + let path = request.path.unwrap_or_default().trim().to_owned(); + if path.is_empty() { + let working_dir = std::env::current_dir().unwrap(); + let current_path = working_dir.to_string_lossy().to_string(); + return Ok( + ResponseBuilder::ok().json(&&LocalStoragePathHelperResponse::CurrentPath(current_path)) + ); + } + let path = std::path::Path::new(&path); + let response = if path.exists() { + // List directories + let mut directories = vec![]; + for entry in std::fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + if let Some(file_name) = path.file_name() { + directories.push(file_name.to_string_lossy().to_string()); + } + } + } + LocalStoragePathHelperResponse::Directories(directories) + } else { + LocalStoragePathHelperResponse::PathDoesNotExist + }; + Ok(ResponseBuilder::ok().json(&response)) +} diff --git a/nitro_repo/src/app/api/storage/s3.rs b/nitro_repo/src/app/api/storage/s3.rs new file mode 100644 index 00000000..4123f6cc --- /dev/null +++ b/nitro_repo/src/app/api/storage/s3.rs @@ -0,0 +1,38 @@ +use axum::{ + response::{IntoResponse, Response}, + routing::get, +}; +use nr_core::user::permissions::HasPermissions; +use nr_storage::s3::regions::S3StorageRegion; +use strum::IntoEnumIterator; +use tracing::instrument; +use utoipa::OpenApi; + +use crate::{ + app::{authentication::Authentication, responses::MissingPermission, NitroRepo}, + error::InternalError, + utils::response_builder::ResponseBuilder, +}; + +#[derive(OpenApi)] +#[openapi(paths(region_list), components(schemas(S3StorageRegion)))] +pub struct S3StorageAPI; +pub fn s3_storage_api() -> axum::Router { + axum::Router::new().route("/regions", get(region_list)) +} + +#[utoipa::path( + get, + path = "/regions", + responses( + (status = 200, description = "A list of available regions for the S3 storage", body = Vec) + ) +)] +#[instrument] +pub async fn region_list(auth: Authentication) -> Result { + if !auth.is_admin_or_system_manager() { + return Ok(MissingPermission::StorageManager.into_response()); + } + let regions: Vec<_> = S3StorageRegion::iter().collect(); + Ok(ResponseBuilder::ok().json(®ions)) +} diff --git a/nitro_repo/src/app/authentication/session.rs b/nitro_repo/src/app/authentication/session.rs index c1f5ae96..fea3703b 100644 --- a/nitro_repo/src/app/authentication/session.rs +++ b/nitro_repo/src/app/authentication/session.rs @@ -1,10 +1,7 @@ use std::{ fmt::Debug, path::PathBuf, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::atomic::{AtomicBool, Ordering}, }; use crate::{ @@ -301,6 +298,7 @@ impl SessionManager { .to_std() .expect("Duration is too large"); debug!("Starting Session Cleaner with interval: {:?}", how_often); + this.session_manager.running.store(true, Ordering::Relaxed); let result = tokio::spawn(async move { let this = this; SessionManager::cleaner_task(this, how_often).await; diff --git a/nitro_repo/src/app/config.rs b/nitro_repo/src/app/config.rs index f0072645..031cada7 100644 --- a/nitro_repo/src/app/config.rs +++ b/nitro_repo/src/app/config.rs @@ -98,6 +98,8 @@ pub struct SiteSetting { pub name: String, pub description: String, pub is_https: bool, + #[cfg(feature = "frontend")] + pub frontend_path: Option, } impl Default for SiteSetting { @@ -107,6 +109,8 @@ impl Default for SiteSetting { name: "Nitro Repo".to_string(), description: "An Open Source artifact manager.".to_string(), is_https: false, + #[cfg(feature = "frontend")] + frontend_path: None, } } } diff --git a/nitro_repo/src/app/email_service.rs b/nitro_repo/src/app/email_service.rs index 7f81332d..302e065c 100644 --- a/nitro_repo/src/app/email_service.rs +++ b/nitro_repo/src/app/email_service.rs @@ -1,7 +1,7 @@ use std::{ fmt::{Debug, Formatter}, io, - sync::{atomic::AtomicBool, Arc}, + sync::Arc, }; use flume::{Receiver, Sender}; diff --git a/nitro_repo/src/app/frontend/hosted.rs b/nitro_repo/src/app/frontend/hosted.rs new file mode 100644 index 00000000..5f36d9c1 --- /dev/null +++ b/nitro_repo/src/app/frontend/hosted.rs @@ -0,0 +1,193 @@ +use std::{ + path::{Path, PathBuf}, + sync::atomic::AtomicBool, +}; + +use axum::{ + extract::{Request, State}, + response::Response, +}; +use handlebars::Handlebars; + +use tracing::{debug, instrument, trace, warn}; + +use crate::{ + app::NitroRepo, + error::InternalError, + utils::{response_builder::ResponseBuilder, responses::APIErrorResponse, TEXT_MEDIA_TYPE}, +}; + +use super::FrontendError; + +#[cfg(feature = "frontend")] +static FRONTEND_DATA: &[u8] = include_bytes!(env!("FRONTEND_ZIP")); +#[derive(Debug)] +pub struct HostedFrontend { + pub frontend_path: PathBuf, + pub enabled: AtomicBool, + pub handlebars: Handlebars<'static>, +} +impl HostedFrontend { + pub fn new(frontend_path: Option) -> Result { + let handlebars = Handlebars::new(); + let frontend_path = frontend_path.unwrap_or_else(|| Path::new("frontend").to_owned()); + Self::save_frontend(frontend_path.clone())?; + let frontend = Self { + frontend_path, + enabled: AtomicBool::new(true), + handlebars: handlebars, + }; + Ok(frontend) + } + fn save_frontend(frontend_path: PathBuf) -> Result<(), FrontendError> { + use std::{ + fs::{self, remove_dir_all, remove_file, File}, + io, + }; + + use zip::ZipArchive; + + // Ensure Directory Is Created + if !frontend_path.exists() { + std::fs::create_dir(&frontend_path)?; + } + // Ensure Directory Is Empty + for entry in std::fs::read_dir(&frontend_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + remove_dir_all(&path)?; + } else { + remove_file(&path)?; + } + } + let reader = std::io::Cursor::new(FRONTEND_DATA); + let mut archive = ZipArchive::new(reader)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = match file.enclosed_name() { + Some(path) => frontend_path.join(path), + None => continue, + }; + + { + let comment = file.comment(); + if !comment.is_empty() { + debug!("File {i} comment: {comment}"); + } + } + + if (*file.name()).ends_with('/') { + debug!("File {} extracted to \"{}\"", i, outpath.display()); + std::fs::create_dir_all(&outpath)?; + } else { + debug!( + "File {} extracted to \"{}\" ({} bytes)", + i, + outpath.display(), + file.size() + ); + if let Some(p) = outpath.parent() { + if !p.exists() { + std::fs::create_dir_all(p)?; + } + } + let mut outfile = File::create(&outpath)?; + io::copy(&mut file, &mut outfile)?; + } + + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; + } + } + } + Ok(()) + } + pub fn does_path_exist(&self, path: &str) -> bool { + let path = self.get_path(path); + trace!(?path, "Checking Path"); + path.exists() + } + pub fn get_path(&self, path: &str) -> PathBuf { + let path = if path.starts_with("/") { + &path[1..] + } else { + path + }; + self.frontend_path.join(path) + } + + pub fn get_index_file(&self, site: &NitroRepo) -> Result, FrontendError> { + let path = self.frontend_path.join("index.html"); + if !path.exists() { + return Err(FrontendError::IndexPageMissing); + } + let content = std::fs::read_to_string(path)?; + let instance = site.instance.lock().clone(); + let rendered = self.handlebars.render_template(&content, &instance)?; + Ok(rendered.into_bytes()) + } + + pub fn get_file_as_response(&self, path: &str) -> Result { + let path = self.get_path(path); + if !path.exists() { + warn!(?path, "File Not Found"); + return Ok(ResponseBuilder::not_found().empty()); + } + let content_type = Self::guess_mime_type(&path); + let file = std::fs::read(path)?; + Ok(ResponseBuilder::ok().content_type(&content_type).body(file)) + } + #[instrument] + fn guess_mime_type(path: &Path) -> String { + match mime_guess::from_path(path).first() { + Some(mime) => { + let mime: &str = mime.as_ref(); + debug!(?mime, "Mime Type"); + mime.to_owned() + } + None => "text/plain".to_owned(), + } + } +} + +#[instrument] +pub async fn frontend_request( + State(site): State, + request: Request, +) -> Result { + let frontend = &site.frontend; + let path = request.uri().path(); + debug!(?path, "Frontend Request"); + + if path.eq("/") || path.eq("") || path.eq("/index.html") || (!frontend.does_path_exist(path)) { + if should_return_404(path) { + return Ok(ResponseBuilder::not_found().empty()); + } + debug!("Returning Index File"); + let index_file = frontend.get_index_file(&site)?; + return Ok(ResponseBuilder::ok().html(index_file)); + } + let response = frontend.get_file_as_response(path)?; + Ok(response) +} +/// Basically if it contains and extension that we want to send a server side 404 from. +/// +/// Such as images, css, js, etc. +fn should_return_404(path: &str) -> bool { + let extensions = vec![ + ".css", ".js", ".png", ".jpg", ".jpeg", ".svg", ".ico", ".webp", ".gif", ".ttf", ".ico", + ]; + for ext in extensions { + if path.ends_with(ext) { + return true; + } + } + false +} diff --git a/nitro_repo/src/app/frontend/mod.rs b/nitro_repo/src/app/frontend/mod.rs new file mode 100644 index 00000000..1476a9f8 --- /dev/null +++ b/nitro_repo/src/app/frontend/mod.rs @@ -0,0 +1,56 @@ +use axum::response::Response; +use http::header::CONTENT_TYPE; +use thiserror::Error; + +use crate::{ + error::IntoErrorResponse, + utils::{response_builder::ResponseBuilder, responses::APIErrorResponse, TEXT_MEDIA_TYPE}, +}; +#[cfg(feature = "frontend")] +mod hosted; +#[cfg(feature = "frontend")] +pub use hosted::*; +#[cfg(not(feature = "frontend"))] +pub use no_frontend::*; + +#[derive(Debug, Error)] +pub enum FrontendError { + #[error("Index Page Missing")] + IndexPageMissing, + #[error("Failed to read frontend data")] + IOError(#[from] std::io::Error), + #[error("File not found")] + FileNotFound, + #[error(transparent)] + HandlebarsError(#[from] handlebars::RenderError), + #[error(transparent)] + ZipError(#[from] zip::result::ZipError), +} +impl IntoErrorResponse for FrontendError { + fn into_response_boxed(self: Box) -> Response { + let response = APIErrorResponse::<(), Box> { + message: "Frontend Error".into(), + error: Some(self), + details: None, + }; + + let response_text = response.to_string(); + + ResponseBuilder::internal_server_error() + .header(CONTENT_TYPE, TEXT_MEDIA_TYPE) + .body(response_text) + } +} +#[cfg(not(feature = "frontend"))] +mod no_frontend { + use axum::extract::{Request, State}; + + use crate::{app::NitroRepo, utils::response_builder::ResponseBuilder}; + + pub async fn frontend_request( + State(_): State, + _request: Request, + ) -> Result { + Ok(ResponseBuilder::not_found().empty()) + } +} diff --git a/nitro_repo/src/app/logging/config/mod.rs b/nitro_repo/src/app/logging/config/mod.rs index 6af3ce4e..16d30ce6 100644 --- a/nitro_repo/src/app/logging/config/mod.rs +++ b/nitro_repo/src/app/logging/config/mod.rs @@ -5,15 +5,11 @@ use ahash::{HashMap, HashMapExt}; use nr_core::logging::{LevelSerde, LoggingLevels}; pub use otel::*; use serde::{Deserialize, Serialize}; -use tracing::level_filters::LevelFilter; use tracing_appender::rolling::Rotation; -use tracing_subscriber::{ - filter::Targets, - fmt::{ +use tracing_subscriber::fmt::{ format::{self, Format}, time::SystemTime, - }, -}; + }; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] diff --git a/nitro_repo/src/app/mod.rs b/nitro_repo/src/app/mod.rs index 798e7239..ff5f1458 100644 --- a/nitro_repo/src/app/mod.rs +++ b/nitro_repo/src/app/mod.rs @@ -10,7 +10,7 @@ use derive_more::{derive::Deref, AsRef, Into}; use email::EmailSetting; use email_service::{EmailAccess, EmailService}; use http::Uri; -use logging::LoggingState; +pub mod frontend; use nr_core::{ database::{ repository::DBRepository, @@ -109,6 +109,8 @@ pub struct NitroRepoInner { pub repositories: RwLock>, pub name_lookup_table: Mutex>, pub general_security_settings: SecuritySettings, + #[cfg(feature = "frontend")] + pub frontend: frontend::HostedFrontend, pub staging_config: StagingConfig, services: Mutex, } @@ -238,7 +240,10 @@ impl NitroRepo { general_security_settings: security, staging_config, services: Mutex::new(services), + #[cfg(feature = "frontend")] + frontend: frontend::HostedFrontend::new(site.frontend_path)?, }; + let session_manager = Arc::new(SessionManager::new(session_manager, mode)?); let nitro_repo = NitroRepo { diff --git a/nitro_repo/src/app/web.rs b/nitro_repo/src/app/web.rs index 83e82065..4475dfce 100644 --- a/nitro_repo/src/app/web.rs +++ b/nitro_repo/src/app/web.rs @@ -2,7 +2,6 @@ use crate::app::logging::request_logging::layer::AppTracingLayer; use super::authentication::api_middleware::AuthenticationLayer; use super::config::{load_config, WebServer}; -use super::logging::LoggingState; use super::{api, config::NitroRepoConfig}; use super::{open_api, NitroRepo}; @@ -89,6 +88,7 @@ pub(crate) async fn start(config_path: Option) -> anyhow::Result<()> { ) .nest("/api", api::api_routes()) .nest("/badge", super::badge::badge_routes()) + .fallback(super::frontend::frontend_request) .with_state(site.clone()); if open_api_routes { diff --git a/nitro_repo/src/error/mod.rs b/nitro_repo/src/error/mod.rs index ed0d5d48..c36e96c8 100644 --- a/nitro_repo/src/error/mod.rs +++ b/nitro_repo/src/error/mod.rs @@ -3,9 +3,13 @@ use std::{error::Error, fmt::Display, io}; use axum::{body::Body, response::IntoResponse}; pub use bad_requests::*; +use http::header::CONTENT_TYPE; use nr_core::repository::config::RepositoryConfigError; //pub use internal_error::*; use nr_storage::StorageError; +use thiserror::Error; + +use crate::utils::TEXT_MEDIA_TYPE; /// Allows creating a response from an error pub trait IntoErrorResponse: Error + Send + Sync { @@ -136,3 +140,29 @@ impl From for InternalError { InternalError(Box::new(err)) } } + +#[derive(Debug, Error)] +pub enum ResponseBuildError { + #[error("Failed to serialize data for response: {0}")] + SerdeError(#[from] serde_json::Error), + #[error("Failed to build response: {0}")] + HttpError(#[from] http::Error), + #[error("Invalid Header Response Value: {0}")] + HeaderValueError(#[from] http::header::InvalidHeaderValue), +} +impl IntoResponse for ResponseBuildError { + fn into_response(self) -> axum::response::Response { + let message = self.to_string(); + http::Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, TEXT_MEDIA_TYPE) + .body(axum::body::Body::from(message)) + .unwrap() + } +} + +impl IntoErrorResponse for ResponseBuildError { + fn into_response_boxed(self: Box) -> axum::response::Response { + self.into_response() + } +} diff --git a/nitro_repo/src/utils.rs b/nitro_repo/src/utils.rs index 9312cad7..2254a193 100644 --- a/nitro_repo/src/utils.rs +++ b/nitro_repo/src/utils.rs @@ -3,11 +3,16 @@ use std::fs::OpenOptions; use std::io::Read; use std::path::Path; +use digestible::{byteorder::NativeEndian, Digester, Digestible, IntoBase64}; +use http::HeaderValue; use rust_embed::RustEmbed; +use sha2::Digest; use tracing::error; - -use crate::error::InternalError; - +pub mod response_builder; +pub mod responses; +use crate::error::{InternalError, ResponseBuildError}; +pub const JSON_MEDIA_TYPE: HeaderValue = HeaderValue::from_static("application/json"); +pub const TEXT_MEDIA_TYPE: HeaderValue = HeaderValue::from_static("text/plain"); #[derive(RustEmbed)] #[folder = "$CARGO_MANIFEST_DIR/resources"] pub struct Resources; @@ -48,3 +53,10 @@ impl Resources { } pub mod headers; + +pub fn generate_etag(data: &impl Digestible) -> Result { + let hasher = sha2::Sha256::new().into_base64(); + let result = hasher.digest::(data); + + Ok(HeaderValue::try_from(result)?) +} diff --git a/nitro_repo/src/utils/response_builder.rs b/nitro_repo/src/utils/response_builder.rs new file mode 100644 index 00000000..471a587a --- /dev/null +++ b/nitro_repo/src/utils/response_builder.rs @@ -0,0 +1,118 @@ +use axum::response::{IntoResponse, Response}; +use digestible::Digestible; +use http::{header::CONTENT_TYPE, HeaderName, HeaderValue, StatusCode}; + +use crate::error::ResponseBuildError; + +use super::{generate_etag, JSON_MEDIA_TYPE}; +macro_rules! new_response_builder { + ( + $( $fn_name:ident => $status:ident),* + ) => { + $( + /// Create a new response builder with the [StatusCode] $status + pub fn $fn_name() -> Self { + Self(Response::builder().status(StatusCode::$status)) + } + )* + }; +} +pub struct ResponseBuilder(pub http::response::Builder); +/// When a body method is called. It converts it to a response. So if you return a response builder it will be empty body +impl IntoResponse for ResponseBuilder { + fn into_response(self) -> Response { + self.empty() + } +} +impl Default for ResponseBuilder { + fn default() -> Self { + Self(Response::builder().status(StatusCode::NO_CONTENT)) + } +} +impl ResponseBuilder { + pub fn status(self, status: StatusCode) -> Self { + Self(self.0.status(status)) + } + new_response_builder!( + ok => OK, + no_content => NO_CONTENT, + bad_request => BAD_REQUEST, + not_found => NOT_FOUND, + conflict => CONFLICT, + unauthorized => UNAUTHORIZED, + forbidden => FORBIDDEN, + internal_server_error => INTERNAL_SERVER_ERROR, + created => CREATED + ); + /// Sets the body if it returns an error it will return a [ResponseBuildError] + pub fn body_or_err( + self, + body: impl Into, + ) -> Result { + let body = body.into(); + self.0.body(body).map_err(ResponseBuildError::HttpError) + } + /// Sets the body if it returns an error it will return a [ResponseBuildError] + pub fn body(self, body: impl Into) -> Response { + match self.body_or_err(body) { + Ok(ok) => ok, + Err(err) => err.into_response(), + } + } + /// Empty body + pub fn empty(self) -> Response { + self.body(axum::body::Body::empty()) + } + pub fn header(self, key: K, value: V) -> Self + where + K: TryInto, + >::Error: Into, + V: TryInto, + >::Error: Into, + { + Self(self.0.header(key, value)) + } + /// Attempts to set the etag header + pub fn etag_or_err(self, data: &D) -> Result { + let etag = generate_etag(data)?; + Ok(self.header(http::header::ETAG, etag)) + } + /// Attempts to set the etag header if the data is present + pub fn optional_etag_or_err( + self, + data: &Option, + ) -> Result { + match data { + Some(data) => self.etag_or_err(data), + None => Ok(self), + } + } + /// Serialize the data to JSON and return a response or an error + pub fn json_or_err( + self, + data: &T, + ) -> Result { + let body = serde_json::to_vec(data)?; + self.header(CONTENT_TYPE, JSON_MEDIA_TYPE).body_or_err(body) + } + /// Serialize the data to JSON and return a response + pub fn json(self, data: &T) -> Response { + match self.json_or_err(data) { + Ok(ok) => ok, + Err(err) => err.into_response(), + } + } + pub fn content_type(self, content_type: &str) -> Self { + self.header(CONTENT_TYPE, content_type) + } + /// Checks if the data is present and returns a JSON response or a not found response + pub fn json_or_not_found(self, data: &Option) -> Response { + match data { + Some(data) => self.json(data), + None => self.status(StatusCode::NOT_FOUND).empty(), + } + } + pub fn html(self, data: impl Into) -> Response { + self.header(CONTENT_TYPE, "text/html").body(data) + } +} diff --git a/nitro_repo/src/utils/responses/conflict.rs b/nitro_repo/src/utils/responses/conflict.rs new file mode 100644 index 00000000..c64cd4e6 --- /dev/null +++ b/nitro_repo/src/utils/responses/conflict.rs @@ -0,0 +1,80 @@ +use std::borrow::Cow; + +use axum::response::{IntoResponse, Response}; +use serde::Serialize; +use serde_json::Value; +use utoipa::ToSchema; + +use crate::utils::response_builder::ResponseBuilder; + +use super::APIErrorResponse; + +#[derive(Serialize, ToSchema)] +pub struct ConflictResponse { + /// Field that caused the conflict + pub field: Cow<'static, str>, +} + +impl utoipa::IntoResponses for ConflictResponse { + fn responses() -> std::collections::BTreeMap< + String, + utoipa::openapi::RefOr, + > { + let missing_permission_response = APIErrorResponse::<&str, ()>::name(); + utoipa::openapi::response::ResponsesBuilder::new() + .responses_from_iter([( + "409", + utoipa::openapi::ResponseBuilder::new() + .description("A conflict occurred") + .content( + "application/json", + utoipa::openapi::content::ContentBuilder::new() + .schema(Some( + utoipa::openapi::schema::RefBuilder::new() + .ref_location_from_schema_name(missing_permission_response), + )) + .example(Some(example())) + .into(), + ) + .build(), + )]) + .build() + .into() + } +} + +fn example() -> Value { + let response: APIErrorResponse<&str, ()> = APIErrorResponse { + message: "Conflict".into(), + details: Some("Some_Field"), + error: None, + }; + serde_json::to_value(response).unwrap() +} + +impl From<&'static str> for ConflictResponse { + fn from(field: &'static str) -> Self { + ConflictResponse { + field: Cow::Borrowed(field), + } + } +} +impl From for ConflictResponse { + fn from(field: String) -> Self { + ConflictResponse { + field: Cow::Owned(field), + } + } +} + +impl IntoResponse for ConflictResponse { + fn into_response(self) -> Response { + let response: APIErrorResponse<&str, ()> = APIErrorResponse { + message: "Conflict".into(), + details: Some(self.field.as_ref()), + error: None, + }; + + ResponseBuilder::conflict().json(&response) + } +} diff --git a/nitro_repo/src/utils/responses/mod.rs b/nitro_repo/src/utils/responses/mod.rs new file mode 100644 index 00000000..e352a72c --- /dev/null +++ b/nitro_repo/src/utils/responses/mod.rs @@ -0,0 +1,93 @@ +mod conflict; +use std::{ + borrow::Cow, + fmt::{Debug, Display}, +}; + +pub use conflict::*; +use serde::{ser::SerializeMap, Serialize, Serializer}; +use utoipa::ToSchema; + +#[derive(Debug, ToSchema)] +pub struct APIErrorResponse> { + /// The message to display to the user + pub message: Cow<'static, str>, + /// The error that caused the issue if any + #[schema(value_type = Option, nullable)] + pub error: Option, + /// Additional details about the error if any + pub details: Option, +} +impl Serialize for APIErrorResponse +where + D: Serialize, + E: Debug, +{ + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut map_serializer = serializer.serialize_map(Some(3))?; + map_serializer.serialize_entry("message", &self.message)?; + if let Some(error) = &self.error { + map_serializer.serialize_entry("error", &format!("{:?}", error))?; + } + if let Some(details) = &self.details { + map_serializer.serialize_entry("details", details)?; + } + map_serializer.end() + } +} +impl Default for APIErrorResponse { + fn default() -> Self { + APIErrorResponse { + message: Cow::Borrowed("Unknown Error"), + error: None, + details: None, + } + } +} +impl From<&'static str> for APIErrorResponse { + fn from(message: &'static str) -> Self { + APIErrorResponse { + message: Cow::Borrowed(message), + error: None, + details: None, + } + } +} + +impl From<(E, &'static str)> for APIErrorResponse<(), E> +where + E: Debug + 'static, +{ + fn from((error, message): (E, &'static str)) -> Self { + APIErrorResponse { + message: Cow::Borrowed(message), + error: Some(error), + details: None, + } + } +} + +impl Display for APIErrorResponse +where + T: Debug, + E: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + message, + error, + details, + } = self; + writeln!(f, "{message}")?; + if let Some(error) = error { + writeln!(f, "Error: {:?}", error)?; + } + if let Some(details) = details { + writeln!(f, "Details: {:?}", details)?; + } + Ok(()) + } +} diff --git a/site/.node-version b/site/.node-version index 85aee5a5..92f279e3 100644 --- a/site/.node-version +++ b/site/.node-version @@ -1 +1 @@ -v20 \ No newline at end of file +v22 \ No newline at end of file diff --git a/site/package-lock.json b/site/package-lock.json index 940ecbac..d5850149 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -45,7 +45,7 @@ "@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue-jsx": "^4.1.1", "@vue/eslint-config-prettier": "^10.1.0", - "@vue/eslint-config-typescript": "^14.2.0", + "@vue/eslint-config-typescript": "^14.3.0", "@vue/tsconfig": "^0.7.0", "browserslist": "^4.24.4", "browserslist-to-esbuild": "^2.1.1", @@ -473,9 +473,9 @@ } }, "node_modules/@babel/standalone": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.26.5.tgz", - "integrity": "sha512-vXbSrFq1WauHvOg/XWcjkF6r7wDSHbN3+3Aro6LYjfODpGw8dCyqqbUMRX5LXlgzVAUrTSN6JkepFiHhLKHV5Q==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.26.6.tgz", + "integrity": "sha512-h1mkoNFYCqDkS+vTLGzsQYvp1v1qbuugk4lOtb/oyjArZ+EtreAaxcSYg3rSIzWZRQOjx4iqGe7A8NRYIMSTTw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -969,18 +969,6 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint-community/regexpp": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", @@ -1004,6 +992,28 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/core": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", @@ -1039,6 +1049,16 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -1051,6 +1071,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", @@ -1274,9 +1306,9 @@ } }, "node_modules/@mdn/browser-compat-data": { - "version": "5.6.29", - "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.6.29.tgz", - "integrity": "sha512-+s2wY7ftjoXf3UwyvR7U4EKDCpUuxlCdnv2OP5BAk1uvoCgUVVU0GtVNolD5Gj+1oVWX1y5a4Yj/LIaThUDmGA==", + "version": "5.6.30", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.6.30.tgz", + "integrity": "sha512-K9TP4Io1qeBIVCNZ3bUPlIpaukqp5SkjZQRpdg8SK2+5buFOmYlxrVoe/33y5eicxpKZjzoJjQjRpRTKgpKYZA==", "license": "CC0-1.0" }, "node_modules/@milkdown/core": { @@ -1476,40 +1508,41 @@ } }, "node_modules/@nuxt/kit": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.15.1.tgz", - "integrity": "sha512-7cVWjzfz3L6CsZrg6ppDZa7zGrZxCSfZjEQDIvVFn4mFKtJlK9k2izf5EewL6luzWwIQojkZAC3iq/1wtgI0Xw==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.15.2.tgz", + "integrity": "sha512-nxiPJVz2fICcyBKlN5pL1IgZVejyArulREsS5HvAk07hijlYuZ5toRM8soLt51VQNpFd/PedL+Z1AlYu/bQCYQ==", "license": "MIT", "dependencies": { - "@nuxt/schema": "3.15.1", + "@nuxt/schema": "3.15.2", "c12": "^2.0.1", - "consola": "^3.3.3", + "consola": "^3.4.0", "defu": "^6.1.4", "destr": "^2.0.3", "globby": "^14.0.2", - "ignore": "^7.0.0", + "ignore": "^7.0.3", "jiti": "^2.4.2", "klona": "^2.0.6", "knitwork": "^1.2.0", - "mlly": "^1.7.3", + "mlly": "^1.7.4", "ohash": "^1.1.4", - "pathe": "^2.0.0", - "pkg-types": "^1.3.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.1", "scule": "^1.3.0", "semver": "^7.6.3", + "std-env": "^3.8.0", "ufo": "^1.5.4", "unctx": "^2.4.1", - "unimport": "^3.14.5", + "unimport": "^3.14.6", "untyped": "^1.5.2" }, "engines": { - "node": ">=18.20.5" + "node": ">=18.0.0" } }, "node_modules/@nuxt/kit/node_modules/ignore": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.0.tgz", - "integrity": "sha512-lcX8PNQygAa22u/0BysEY8VhaFRzlOkvdlKczDPnJvrkJD1EuqzEky5VYYKM2iySIuaVIDv9N190DfSreSLw2A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "license": "MIT", "engines": { "node": ">= 4" @@ -1528,14 +1561,14 @@ } }, "node_modules/@nuxt/schema": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.15.1.tgz", - "integrity": "sha512-n5kOHt8uUyUM9z4Wu/8tIZkBYh3KTCGvyruG6oD9bfeT4OaS21+X3M7XsTXFMe+eYBZA70IFFlWn1JJZIPsKeA==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.15.2.tgz", + "integrity": "sha512-cTHGbLTbrQ83B+7Mh0ggc5MzIp74o8KciA0boCiBJyK5uImH9QQNK6VgfwRWcTD5sj3WNKiIB1luOMom3LHgVw==", "license": "MIT", "dependencies": { - "consola": "^3.3.3", + "consola": "^3.4.0", "defu": "^6.1.4", - "pathe": "^2.0.0", + "pathe": "^2.0.1", "std-env": "^3.8.0" }, "engines": { @@ -2287,9 +2320,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, "license": "MIT", "dependencies": { @@ -2309,17 +2342,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", - "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", + "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.19.1", - "@typescript-eslint/type-utils": "8.19.1", - "@typescript-eslint/utils": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/type-utils": "8.20.0", + "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2339,16 +2372,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", - "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", + "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.19.1", - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/typescript-estree": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4" }, "engines": { @@ -2364,14 +2397,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", - "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", + "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1" + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2382,14 +2415,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", - "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", + "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.19.1", - "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/utils": "8.20.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.0" }, @@ -2406,9 +2439,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", - "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", + "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", "dev": true, "license": "MIT", "engines": { @@ -2420,14 +2453,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", - "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", + "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/visitor-keys": "8.19.1", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2446,32 +2479,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -2486,16 +2493,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", - "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.19.1", - "@typescript-eslint/types": "8.19.1", - "@typescript-eslint/typescript-estree": "8.19.1" + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2510,13 +2517,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", - "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", + "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/types": "8.20.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2527,6 +2534,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz", @@ -2774,14 +2794,15 @@ } }, "node_modules/@vue/eslint-config-typescript": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.2.0.tgz", - "integrity": "sha512-JJ4wHuTJa2faQsBOUeWzuHOSFizVS7RWG2eH2noABk2LcT4wVcTOMZKM/lFobKBcgwADIPAKVRGFHVKooXImoA==", + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.3.0.tgz", + "integrity": "sha512-bOreIxlSC/xsUdhDdKIHb1grwJah+IokNeJ50LqA1StdOHeSPUxSIPNxyKgRx4YdjhyzC6TKtrCf6yYK99x3Uw==", "dev": true, "license": "MIT", "dependencies": { - "fast-glob": "^3.3.2", - "typescript-eslint": "^8.18.1", + "@typescript-eslint/utils": "^8.20.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.20.0", "vue-eslint-parser": "^9.4.3" }, "engines": { @@ -2823,32 +2844,6 @@ } } }, - "node_modules/@vue/language-core/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@vue/language-core/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@vue/reactivity": { "version": "3.5.13", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", @@ -3181,13 +3176,13 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -3443,9 +3438,9 @@ "license": "MIT" }, "node_modules/consola": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.3.3.tgz", - "integrity": "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", "license": "MIT", "engines": { "node": "^14.18.0 || >=16.10.0" @@ -3668,9 +3663,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.80", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.80.tgz", - "integrity": "sha512-LTrKpW0AqIuHwmlVNV+cjFYTnXtM9K37OGhpe0ZI10ScPSxqVSryZHIY3WnCS5NSYbBODRTZyhRMS2h5FAEqAw==", + "version": "1.5.83", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz", + "integrity": "sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==", "license": "ISC" }, "node_modules/entities": { @@ -3876,9 +3871,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz", + "integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3975,17 +3970,27 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3998,6 +4003,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", @@ -4015,6 +4044,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -4273,9 +4314,9 @@ } }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "dev": true, "license": "MIT", "dependencies": { @@ -4814,13 +4855,13 @@ } }, "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.0.0.tgz", + "integrity": "sha512-bbgPw/wmroJsil/GgL4qjDzs5YLTBMQ99weRsok1XCDccQeehbHA/I1oRvk2NPtr7KGZgT/Y5tPRnAtMqeG2Kg==", "license": "MIT", "dependencies": { "mlly": "^1.7.3", - "pkg-types": "^1.2.1" + "pkg-types": "^1.3.0" }, "engines": { "node": ">=14" @@ -5544,15 +5585,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -5615,23 +5660,17 @@ } }, "node_modules/mlly": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", - "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", "license": "MIT", "dependencies": { "acorn": "^8.14.0", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, - "node_modules/mlly/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "license": "MIT" - }, "node_modules/mrmime": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", @@ -5757,16 +5796,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/npm-run-all2/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/npm-run-all2/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -5777,22 +5806,6 @@ "node": ">=16" } }, - "node_modules/npm-run-all2/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/npm-run-all2/node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -6120,26 +6133,20 @@ } }, "node_modules/pkg-types": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.0.tgz", - "integrity": "sha512-kS7yWjVFCkIw9hqdJBoMxDdzEngmkr5FXeWZZfQ6GoYacjVnsW6l2CcYW/0ThD0vF4LPJgVYnrg4d0uuhwYQbg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "dependencies": { "confbox": "^0.1.8", - "mlly": "^1.7.3", - "pathe": "^1.1.2" + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "license": "MIT" - }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "funding": [ { "type": "opencollective", @@ -6156,7 +6163,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6631,9 +6638,9 @@ } }, "node_modules/sass": { - "version": "1.83.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", - "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", "dev": true, "license": "MIT", "dependencies": { @@ -6974,15 +6981,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.19.1.tgz", - "integrity": "sha512-LKPUQpdEMVOeKluHi8md7rwLcoXHhwvWp3x+sJkMuq3gGm9yaYJtPo8sRZSblMFJ5pcOGCAak/scKf1mvZDlQw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.20.0.tgz", + "integrity": "sha512-Kxz2QRFsgbWj6Xcftlw3Dd154b3cEPFqQC+qMZrMypSijPd4UanKKvoKDrJ4o8AIfZFKAF+7sMaEIR8mTElozA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.19.1", - "@typescript-eslint/parser": "8.19.1", - "@typescript-eslint/utils": "8.19.1" + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", + "@typescript-eslint/utils": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7068,25 +7075,25 @@ } }, "node_modules/unimport": { - "version": "3.14.5", - "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.5.tgz", - "integrity": "sha512-tn890SwFFZxqaJSKQPPd+yygfKSATbM8BZWW1aCR2TJBTs1SDrmLamBueaFtYsGjHtQaRgqEbQflOjN2iW12gA==", + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.3", + "@rollup/pluginutils": "^5.1.4", "acorn": "^8.14.0", "escape-string-regexp": "^5.0.0", "estree-walker": "^3.0.3", - "fast-glob": "^3.3.2", - "local-pkg": "^0.5.1", - "magic-string": "^0.30.14", - "mlly": "^1.7.3", - "pathe": "^1.1.2", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", "picomatch": "^4.0.2", - "pkg-types": "^1.2.1", + "pkg-types": "^1.3.0", "scule": "^1.3.0", "strip-literal": "^2.1.1", - "unplugin": "^1.16.0" + "unplugin": "^1.16.1" } }, "node_modules/unimport/node_modules/estree-walker": { @@ -7098,12 +7105,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/unimport/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "license": "MIT" - }, "node_modules/unimport/node_modules/picomatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", @@ -7701,19 +7702,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/vue-eslint-parser/node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", diff --git a/site/package.json b/site/package.json index eb696712..c8819e38 100644 --- a/site/package.json +++ b/site/package.json @@ -61,7 +61,7 @@ "eslint": "^9.18.0", "eslint-plugin-vue": "^9.32.0", "@vue/eslint-config-prettier": "^10.1.0", - "@vue/eslint-config-typescript": "^14.2.0", + "@vue/eslint-config-typescript": "^14.3.0", "browserslist": "^4.24.4", "browserslist-to-esbuild": "^2.1.1" }