diff --git a/.github/workflows/checkFrontend.yml b/.github/workflows/checkFrontend.yml new file mode 100644 index 000000000..75b235fe8 --- /dev/null +++ b/.github/workflows/checkFrontend.yml @@ -0,0 +1,34 @@ +name: checkFrontend.yml +permissions: + contents: read +on: + push: + +jobs: + check-frontend: + name: Check Frontend + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '24' + - name: Set up pnpm + uses: pnpm/action-setup@v2 + with: + version: 10 + - name: Install dependencies + working-directory: ui + run: pnpm install + - name: Test frontend + working-directory: ui + run: pnpm run test + - name: Run tsc + working-directory: ui + run: pnpm run tsc:check + - name: Build project + working-directory: ui + run: pnpm run build diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index f2cc5252a..a3889e9ac 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -21,4 +21,4 @@ jobs: uses: "pascalgn/automerge-action@v0.16.3" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - MERGE_METHOD: squash \ No newline at end of file + MERGE_METHOD: squash diff --git a/Cargo.toml b/Cargo.toml index e469e9c64..e22820f3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "podfetch" version = "0.1.0" -edition = "2021" +edition = "2024" build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/adapters/api/models/podcast_episode_dto.rs b/src/adapters/api/models/podcast_episode_dto.rs index 0bd8b56da..096c043e5 100644 --- a/src/adapters/api/models/podcast_episode_dto.rs +++ b/src/adapters/api/models/podcast_episode_dto.rs @@ -189,12 +189,11 @@ fn map_url( &ENVIRONMENT_SERVICE.server_url )) .unwrap(); - if ENVIRONMENT_SERVICE.any_auth_enabled { - if let Some(user) = user { - if let Some(key) = &user.api_key { - url.query_pairs_mut().append_pair("apiKey", key); - } - } + if ENVIRONMENT_SERVICE.any_auth_enabled + && let Some(user) = user + && let Some(key) = &user.api_key + { + url.query_pairs_mut().append_pair("apiKey", key); } url.query_pairs_mut() .append_pair("episodeId", &episode.episode_id); diff --git a/src/adapters/file/file_handle_wrapper.rs b/src/adapters/file/file_handle_wrapper.rs index 120d3a82a..c7efa30bd 100644 --- a/src/adapters/file/file_handle_wrapper.rs +++ b/src/adapters/file/file_handle_wrapper.rs @@ -54,19 +54,15 @@ impl FileHandleWrapper { if let Some(download_type) = &episode.download_location { let file_type = FileHandlerType::from(download_type.as_str()); if FileHandlerType::S3 == file_type { - if let Some(file_path) = &episode.file_image_path { - if let Err(e) = S3Handler::remove_file(file_path) { - log::error!( - "Error removing file: {file_path} with reason {e}" - ); - } + if let Some(file_path) = &episode.file_image_path + && let Err(e) = S3Handler::remove_file(file_path) + { + log::error!("Error removing file: {file_path} with reason {e}"); } - if let Some(file_path) = &episode.file_episode_path { - if let Err(e) = S3Handler::remove_file(file_path) { - log::error!( - "Error removing file: {file_path} with reason {e}" - ); - } + if let Some(file_path) = &episode.file_episode_path + && let Err(e) = S3Handler::remove_file(file_path) + { + log::error!("Error removing file: {file_path} with reason {e}"); } } } @@ -83,30 +79,26 @@ impl FileHandleWrapper { if let Some(download_type) = &episode.download_location { let file_type = FileHandlerType::from(download_type.as_str()); if FileHandlerType::S3 == file_type { - if let Some(file_path) = &episode.file_image_path { - if let Err(e) = S3Handler::remove_file(file_path) { - log::error!( - "Error removing file: {file_path} with reason {e}" - ); - } + if let Some(file_path) = &episode.file_image_path + && let Err(e) = S3Handler::remove_file(file_path) + { + log::error!("Error removing file: {file_path} with reason {e}"); } - if let Some(file_path) = &episode.file_episode_path { - if let Err(e) = S3Handler::remove_file(file_path) { - log::error!("Error removing file: {file_path} {e}"); - } + if let Some(file_path) = &episode.file_episode_path + && let Err(e) = S3Handler::remove_file(file_path) + { + log::error!("Error removing file: {file_path} {e}"); } } else { - if let Some(file_path) = &episode.file_image_path { - if let Err(e) = LocalFileHandler::remove_file(file_path) { - log::error!( - "Error removing file: {file_path} with reason {e}" - ); - } + if let Some(file_path) = &episode.file_image_path + && let Err(e) = LocalFileHandler::remove_file(file_path) + { + log::error!("Error removing file: {file_path} with reason {e}"); } - if let Some(file_path) = &episode.file_episode_path { - if let Err(e) = LocalFileHandler::remove_file(file_path) { - log::error!("Error removing file: {file_path} {e}"); - } + if let Some(file_path) = &episode.file_episode_path + && let Err(e) = LocalFileHandler::remove_file(file_path) + { + log::error!("Error removing file: {file_path} {e}"); } } } diff --git a/src/adapters/file/local_file_handler.rs b/src/adapters/file/local_file_handler.rs index c4d64f208..5529b6cd6 100644 --- a/src/adapters/file/local_file_handler.rs +++ b/src/adapters/file/local_file_handler.rs @@ -1,5 +1,5 @@ use crate::adapters::file::file_handler::{FileHandler, FileRequest}; -use crate::utils::error::{map_io_error, CustomError, ErrorSeverity}; +use crate::utils::error::{CustomError, ErrorSeverity, map_io_error}; use std::fs::File; use std::future::Future; use std::io; diff --git a/src/adapters/file/s3_file_handler.rs b/src/adapters/file/s3_file_handler.rs index a42551aa9..5c2fd0507 100644 --- a/src/adapters/file/s3_file_handler.rs +++ b/src/adapters/file/s3_file_handler.rs @@ -1,5 +1,5 @@ use crate::adapters::file::file_handler::{FileHandler, FileRequest}; -use crate::utils::error::{map_s3_error, CustomError, ErrorSeverity}; +use crate::utils::error::{CustomError, ErrorSeverity, map_s3_error}; use futures_util::TryFutureExt; use std::future::Future; use std::pin::Pin; diff --git a/src/adapters/persistence/dbconfig/db.rs b/src/adapters/persistence/dbconfig/db.rs index feb9381f5..284e834b6 100644 --- a/src/adapters/persistence/dbconfig/db.rs +++ b/src/adapters/persistence/dbconfig/db.rs @@ -1,8 +1,8 @@ use crate::adapters::persistence::dbconfig::DBType; use crate::commands::startup::DbPool; use crate::constants::inner_constants::ENVIRONMENT_SERVICE; -use diesel::r2d2::ConnectionManager; use diesel::Connection; +use diesel::r2d2::ConnectionManager; use r2d2::Pool; use std::process::exit; use std::sync::OnceLock; diff --git a/src/adapters/persistence/dbconfig/mod.rs b/src/adapters/persistence/dbconfig/mod.rs index c3db1e49f..415811610 100644 --- a/src/adapters/persistence/dbconfig/mod.rs +++ b/src/adapters/persistence/dbconfig/mod.rs @@ -2,7 +2,6 @@ pub mod db; #[path = "schemas/sqlite/schema.rs"] pub mod schema; - #[derive(diesel::MultiConnection)] pub enum DBType { #[cfg(feature = "postgresql")] @@ -33,11 +32,11 @@ macro_rules! execute_with_conn { let _ = match conn.deref_mut() { #[cfg(feature = "sqlite")] $crate::adapters::persistence::dbconfig::DBType::Sqlite(conn) => { - return $diesel_func(conn) + return $diesel_func(conn); } #[cfg(feature = "postgresql")] $crate::adapters::persistence::dbconfig::DBType::Postgresql(conn) => { - return $diesel_func(conn) + return $diesel_func(conn); } }; }}; diff --git a/src/adapters/persistence/repositories/device/device_repository.rs b/src/adapters/persistence/repositories/device/device_repository.rs index 19223e763..d00b35ffe 100644 --- a/src/adapters/persistence/repositories/device/device_repository.rs +++ b/src/adapters/persistence/repositories/device/device_repository.rs @@ -2,7 +2,7 @@ use crate::adapters::persistence::model::device::device_entity::DeviceEntity; use crate::application::repositories::device_repository::DeviceRepository; use crate::domain::models::device::model::Device; use crate::execute_with_conn; -use crate::utils::error::{map_db_error, CustomError, ErrorSeverity}; +use crate::utils::error::{CustomError, ErrorSeverity, map_db_error}; use diesel::RunQueryDsl; pub struct DeviceRepositoryImpl; diff --git a/src/auth_middleware.rs b/src/auth_middleware.rs index 6e99d2466..b8960add8 100644 --- a/src/auth_middleware.rs +++ b/src/auth_middleware.rs @@ -8,10 +8,10 @@ use axum::extract::Request; use axum::http::HeaderValue; use axum::middleware::Next; use axum::response::Response; -use base64::engine::general_purpose; use base64::Engine; +use base64::engine::general_purpose; use jsonwebtoken::jwk::{JwkSet, KeyAlgorithm}; -use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode}; use log::info; use serde_json::Value; use sha256::digest; diff --git a/src/command_line_runner.rs b/src/command_line_runner.rs index 8e883346c..a834c37e2 100644 --- a/src/command_line_runner.rs +++ b/src/command_line_runner.rs @@ -17,7 +17,7 @@ use crate::utils::time::get_current_timestamp_str; use log::error; use sha256::digest; use std::env::Args; -use std::io::{stdin, Error, ErrorKind}; +use std::io::{Error, ErrorKind, stdin}; use std::process::exit; pub async fn start_command_line(mut args: Args) -> Result<(), CustomError> { diff --git a/src/commands/startup.rs b/src/commands/startup.rs index 0ac43f6f1..5a3dd1e44 100644 --- a/src/commands/startup.rs +++ b/src/commands/startup.rs @@ -1,6 +1,6 @@ use crate::adapters::api::controllers::routes::global_routes; -use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::DBType; +use crate::adapters::persistence::dbconfig::db::get_connection; use crate::auth_middleware::{ handle_basic_auth, handle_no_auth, handle_oidc_auth, handle_proxy_auth, }; @@ -26,20 +26,20 @@ use crate::service::file_service::FileService; use crate::service::podcast_episode_service::PodcastEpisodeService; use crate::service::rust_service::PodcastService; use crate::utils::error::{CustomError, CustomErrorInner}; +use axum::Router; use axum::body::Body; use axum::extract::Request; use axum::middleware::from_fn; use axum::response::{IntoResponse, Redirect, Response}; use axum::routing::get; -use axum::Router; use clokwerk::{Scheduler, TimeUnits}; use diesel::r2d2::ConnectionManager; use diesel_migrations::MigrationHarness; use log::info; -use maud::{html, Markup}; +use maud::{Markup, html}; use r2d2::Pool; -use socketioxide::extract::SocketRef; use socketioxide::SocketIoBuilder; +use socketioxide::extract::SocketRef; use std::ops::DerefMut; use std::process::exit; use std::sync::OnceLock; @@ -55,9 +55,9 @@ use utoipa_scalar::Servable as UtoipaServable; use utoipa_swagger_ui::SwaggerUi; pub type DbPool = Pool>; +use crate::EmbeddedMigrations; use crate::embed_migrations; use crate::utils::error::ErrorSeverity::Warning; -use crate::EmbeddedMigrations; import_database_config!(); @@ -167,7 +167,9 @@ pub fn check_server_config() { if ENVIRONMENT_SERVICE.http_basic && (ENVIRONMENT_SERVICE.password.is_none() || ENVIRONMENT_SERVICE.username.is_none()) { - eprintln!("BASIC_AUTH activated but no username or password set. Please set username and password in the .env file."); + eprintln!( + "BASIC_AUTH activated but no username or password set. Please set username and password in the .env file." + ); exit(1); } @@ -176,12 +178,16 @@ pub fn check_server_config() { || ENVIRONMENT_SERVICE.oidc_configured || ENVIRONMENT_SERVICE.reverse_proxy) { - eprintln!("GPODDER_INTEGRATION_ENABLED activated but no BASIC_AUTH or OIDC_AUTH set. Please set BASIC_AUTH or OIDC_AUTH in the .env file."); + eprintln!( + "GPODDER_INTEGRATION_ENABLED activated but no BASIC_AUTH or OIDC_AUTH set. Please set BASIC_AUTH or OIDC_AUTH in the .env file." + ); exit(1); } if check_if_multiple_auth_is_configured() { - eprintln!("You cannot have oidc and basic auth enabled at the same time. Please disable one of them."); + eprintln!( + "You cannot have oidc and basic auth enabled at the same time. Please disable one of them." + ); exit(1); } } @@ -295,7 +301,7 @@ pub fn run_migrations() { match conn { #[cfg(feature = "postgresql")] - DBType::Postgresql(ref mut conn) => { + DBType::Postgresql(conn) => { let res_migration = conn.run_pending_migrations(POSTGRES_MIGRATIONS); if res_migration.is_err() { @@ -303,7 +309,7 @@ pub fn run_migrations() { } } #[cfg(feature = "sqlite")] - DBType::Sqlite(ref mut conn) => { + DBType::Sqlite(conn) => { let res_migration = conn.run_pending_migrations(SQLITE_MIGRATIONS); if res_migration.is_err() { @@ -424,10 +430,10 @@ pub mod tests { #[cfg(feature = "postgresql")] use crate::commands::startup::handle_config_for_server_startup; #[cfg(feature = "postgresql")] - use testcontainers::runners::AsyncRunner; - #[cfg(feature = "postgresql")] use testcontainers::ContainerAsync; #[cfg(feature = "postgresql")] + use testcontainers::runners::AsyncRunner; + #[cfg(feature = "postgresql")] use testcontainers_modules::postgres::Postgres; pub struct TestServerWrapper<'a> { diff --git a/src/constants/inner_constants.rs b/src/constants/inner_constants.rs index 2895bee50..43b52846d 100644 --- a/src/constants/inner_constants.rs +++ b/src/constants/inner_constants.rs @@ -113,6 +113,7 @@ pub const SUB_DIRECTORY: &str = "SUB_DIRECTORY"; pub const POLLING_INTERVAL: &str = "POLLING_INTERVAL"; pub const STANDARD_USER: &str = "user123"; +pub const STANDARD_USER_ID: i32 = 9999; pub const PODCAST_FILENAME: &str = "podcast"; pub const PODCAST_IMAGENAME: &str = "image"; diff --git a/src/controllers/manifest_controller.rs b/src/controllers/manifest_controller.rs index cb6ceb9fc..d1fedca89 100644 --- a/src/controllers/manifest_controller.rs +++ b/src/controllers/manifest_controller.rs @@ -1,7 +1,7 @@ use crate::constants::inner_constants::ENVIRONMENT_SERVICE; use crate::utils::error::CustomError; -use axum::routing::get; use axum::Json; +use axum::routing::get; use utoipa_axum::router::OpenApiRouter; #[derive(Serialize)] diff --git a/src/controllers/podcast_controller.rs b/src/controllers/podcast_controller.rs index f4f669176..e7b280dfa 100644 --- a/src/controllers/podcast_controller.rs +++ b/src/controllers/podcast_controller.rs @@ -13,13 +13,13 @@ use axum::body::Body; use axum::extract::{Path, Query}; use axum::http::StatusCode; use axum::response::IntoResponse; -use axum::{debug_handler, Extension, Json}; +use axum::{Extension, Json, debug_handler}; use axum_extra::extract::OptionalQuery; -use opml::{Outline, OPML}; -use rand::rngs::ThreadRng; +use opml::{OPML, Outline}; use rand::Rng; +use rand::rngs::ThreadRng; use rss::Channel; -use serde_json::{from_str, Value}; +use serde_json::{Value, from_str}; use std::thread; use tokio::task::spawn_blocking; @@ -29,12 +29,11 @@ use crate::models::podcast_episode::PodcastEpisode; use crate::models::podcast_rssadd_model::PodcastRSSAddModel; use crate::models::podcasts::Podcast; use crate::models::user::User; -use crate::service::file_service::{perform_podcast_variable_replacement, FileService}; +use crate::service::file_service::{FileService, perform_podcast_variable_replacement}; use crate::utils::append_to_header::add_basic_auth_headers_conditionally; use reqwest::header::HeaderMap; use tokio::runtime::Runtime; - #[derive(Serialize, Deserialize, IntoParams)] #[serde(rename_all = "camelCase")] pub struct PodcastSearchModelUtoipa { @@ -628,7 +627,7 @@ use utoipa::{IntoParams, ToSchema}; use utoipa_axum::router::OpenApiRouter; use utoipa_axum::routes; -use crate::utils::error::{map_reqwest_error, CustomError, CustomErrorInner, ErrorSeverity}; +use crate::utils::error::{CustomError, CustomErrorInner, ErrorSeverity, map_reqwest_error}; use crate::utils::http_client::get_http_client; use crate::utils::rss_feed_parser::PodcastParsed; diff --git a/src/controllers/server.rs b/src/controllers/server.rs index cc6ad59dc..f3e6597c0 100644 --- a/src/controllers/server.rs +++ b/src/controllers/server.rs @@ -1,5 +1,5 @@ use crate::adapters::api::models::podcast_episode_dto::PodcastEpisodeDto; -use crate::constants::inner_constants::{PodcastType, MAIN_ROOM}; +use crate::constants::inner_constants::{MAIN_ROOM, PodcastType}; use crate::models::favorite_podcast_episode::FavoritePodcastEpisode; use crate::models::podcast_dto::PodcastDto; use crate::models::podcast_episode::PodcastEpisode; diff --git a/src/controllers/sys_info_controller.rs b/src/controllers/sys_info_controller.rs index f26d643a7..c21eb7aca 100644 --- a/src/controllers/sys_info_controller.rs +++ b/src/controllers/sys_info_controller.rs @@ -53,7 +53,7 @@ pub async fn get_sys_info() -> Result, CustomError> { use crate::constants::inner_constants::ENVIRONMENT_SERVICE; use crate::models::settings::ConfigModel; use crate::utils::error::ErrorSeverity::Info; -use crate::utils::error::{map_io_extra_error, CustomError, CustomErrorInner, ErrorSeverity}; +use crate::utils::error::{CustomError, CustomErrorInner, ErrorSeverity, map_io_extra_error}; use utoipa::ToSchema; use utoipa_axum::router::OpenApiRouter; use utoipa_axum::routes; @@ -165,14 +165,12 @@ pub async fn login(auth: Json) -> Result use crate::ENVIRONMENT_SERVICE; let digested_password = digest(auth.0.password); - if let Some(admin_username) = &ENVIRONMENT_SERVICE.username { - if admin_username == &auth.0.username { - if let Some(admin_password) = &ENVIRONMENT_SERVICE.password { - if admin_password == &digested_password { - return Ok(StatusCode::OK); - } - } - } + if let Some(admin_username) = &ENVIRONMENT_SERVICE.username + && admin_username == &auth.0.username + && let Some(admin_password) = &ENVIRONMENT_SERVICE.password + && admin_password == &digested_password + { + return Ok(StatusCode::OK); } let db_user = User::find_by_username(&auth.0.username)?; diff --git a/src/controllers/user_controller.rs b/src/controllers/user_controller.rs index fdd606c80..092cacabc 100644 --- a/src/controllers/user_controller.rs +++ b/src/controllers/user_controller.rs @@ -1,11 +1,13 @@ -use crate::constants::inner_constants::{Role, ENVIRONMENT_SERVICE}; +use crate::constants::inner_constants::{ + ENVIRONMENT_SERVICE, Role, STANDARD_USER, STANDARD_USER_ID, +}; use crate::models::user::{User, UserWithAPiKey, UserWithoutPassword}; use axum::extract::Path; use axum::{Extension, Json}; use reqwest::StatusCode; use crate::service::user_management_service::UserManagementService; -use crate::utils::error::{CustomError, CustomErrorInner, ErrorSeverity}; +use crate::utils::error::{ApiError, CustomError, CustomErrorInner, ErrorSeverity, ErrorType}; use utoipa::ToSchema; use utoipa_axum::router::OpenApiRouter; @@ -86,13 +88,13 @@ tag="user" pub async fn get_user( Path(username): Path, Extension(requester): Extension, -) -> Result, CustomError> { +) -> Result, ErrorType> { if username == requester.username || username == "me" { return Ok(Json(User::map_to_api_dto(requester))); } if !requester.is_admin() || requester.username != username { - return Err(CustomErrorInner::Forbidden(ErrorSeverity::Warning).into()); + return Err(CustomErrorInner::Forbidden(Warning).into()); } let user = User::find_by_username(&username.clone())?; @@ -139,20 +141,17 @@ pub async fn update_user( Extension(user): Extension, Path(username): Path, user_update: Json, -) -> Result, CustomError> { +) -> Result, ErrorType> { + if STANDARD_USER_ID == user.id { + return Err(ApiError::updating_admin_not_allowed(STANDARD_USER).into()); + } + let old_username = &user.clone().username; if old_username != &username { - return Err(CustomErrorInner::Forbidden(ErrorSeverity::Warning).into()); + return Err(CustomErrorInner::Forbidden(Warning).into()); } - let mut user = User::find_by_username(&username)?; - if let Some(admin_username) = ENVIRONMENT_SERVICE.username.clone() { - if admin_username == user.username { - return Err( - CustomErrorInner::Conflict("Cannot update admin user".to_string(), Info).into(), - ); - } - } + let mut user = User::find_by_username(&username)?; if old_username != &user_update.username && !ENVIRONMENT_SERVICE.oidc_configured { // Check if this username is already taken diff --git a/src/controllers/websocket_controller.rs b/src/controllers/websocket_controller.rs index 526ced543..496201f4a 100644 --- a/src/controllers/websocket_controller.rs +++ b/src/controllers/websocket_controller.rs @@ -111,7 +111,7 @@ pub async fn get_rss_feed( } fn add_api_key_to_url(url: String, api_key: &Option) -> String { - if let Some(ref api_key) = api_key { + if let Some(api_key) = &api_key { if url.contains('?') { return format!("{url}&apiKey={api_key}"); } @@ -263,15 +263,15 @@ fn get_podcast_items_rss(downloaded_episodes: &[PodcastEpisodeDto]) -> Vec .permalink(false) .value(episode.clone().episode_id) .build(); - let item = ItemBuilder::default() + + ItemBuilder::default() .guid(Some(guid)) .pub_date(Some(episode.clone().date_of_recording)) .title(Some(episode.clone().name)) .description(Some(episode.clone().description)) .enclosure(Some(enclosure)) .itunes_ext(itunes_extension) - .build(); - item + .build() }) .collect::>() } diff --git a/src/db.rs b/src/db.rs index 41fa6caee..8ca3751c1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -11,10 +11,10 @@ use crate::models::podcast_episode::PodcastEpisode; use crate::models::podcasts::Podcast; use crate::models::user::User; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; +use diesel::RunQueryDsl; use diesel::dsl::max; use diesel::prelude::*; -use diesel::RunQueryDsl; #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/src/gpodder/auth/authentication.rs b/src/gpodder/auth/authentication.rs index 60f89e205..4fb5f0960 100644 --- a/src/gpodder/auth/authentication.rs +++ b/src/gpodder/auth/authentication.rs @@ -7,8 +7,8 @@ use crate::utils::error::ErrorSeverity::Warning; use crate::utils::error::{CustomError, CustomErrorInner}; use axum::extract::Path; use axum::http::StatusCode; -use axum_extra::extract::cookie::{Cookie, SameSite}; use axum_extra::extract::CookieJar; +use axum_extra::extract::cookie::{Cookie, SameSite}; use sha256::digest; #[utoipa::path( @@ -103,16 +103,16 @@ fn handle_gpodder_basic_auth( return Err(CustomErrorInner::Forbidden(Warning).into()); } - if let Some(admin_username) = &ENVIRONMENT_SERVICE.username { - if admin_username == username { - return Err(CustomErrorInner::Conflict( - "The user you are trying to login is equal to the admin user. Please\ + if let Some(admin_username) = &ENVIRONMENT_SERVICE.username + && admin_username == username + { + return Err(CustomErrorInner::Conflict( + "The user you are trying to login is equal to the admin user. Please\ use another user to login." - .to_string(), - Warning, - ) - .into()); - } + .to_string(), + Warning, + ) + .into()); } let user = User::find_by_username(username)?; diff --git a/src/gpodder/device/device_controller.rs b/src/gpodder/device/device_controller.rs index 55164dfcf..4d8e3ba99 100644 --- a/src/gpodder/device/device_controller.rs +++ b/src/gpodder/device/device_controller.rs @@ -28,8 +28,8 @@ pub async fn post_device( Extension(flag): Extension, Json(device_post): Json, ) -> Result, CustomError> { - let username = &query.0 .0; - let deviceid = trim_from_path(&query.0 .1); + let username = &query.0.0; + let deviceid = trim_from_path(&query.0.1); if &flag.username != username { return Err(CustomErrorInner::Forbidden(Warning).into()); } diff --git a/src/gpodder/parametrization.rs b/src/gpodder/parametrization.rs index 4f59d3d08..9782cb645 100644 --- a/src/gpodder/parametrization.rs +++ b/src/gpodder/parametrization.rs @@ -1,6 +1,6 @@ use crate::constants::inner_constants::ENVIRONMENT_SERVICE; -use axum::routing::get; use axum::Json; +use axum::routing::get; use utoipa_axum::router::OpenApiRouter; #[derive(Serialize, Deserialize)] diff --git a/src/gpodder/session_middleware.rs b/src/gpodder/session_middleware.rs index 6544e87c8..f63dc69fb 100644 --- a/src/gpodder/session_middleware.rs +++ b/src/gpodder/session_middleware.rs @@ -4,7 +4,7 @@ use axum_extra::extract::cookie::CookieJar; use std::convert::Infallible; use crate::utils::error::ErrorSeverity::{Critical, Warning}; -use crate::utils::error::{map_db_error, CustomError, CustomErrorInner}; +use crate::utils::error::{CustomError, CustomErrorInner, map_db_error}; use axum::extract::Request; use axum::middleware::Next; use axum::response::IntoResponse; diff --git a/src/gpodder/subscription/subscriptions.rs b/src/gpodder/subscription/subscriptions.rs index dffd427ff..8d27fd41f 100644 --- a/src/gpodder/subscription/subscriptions.rs +++ b/src/gpodder/subscription/subscriptions.rs @@ -7,7 +7,7 @@ use crate::utils::time::get_current_timestamp; use axum::extract::{Path, Query}; use axum::response::IntoResponse; use axum::{Extension, Json}; -use opml::{Outline, OPML}; +use opml::{OPML, Outline}; use serde::Serialize; use utoipa::ToSchema; use utoipa_axum::router::OpenApiRouter; diff --git a/src/main.rs b/src/main.rs index 2123a697b..24b72bf63 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use diesel_migrations::{embed_migrations, EmbeddedMigrations}; +use diesel_migrations::{EmbeddedMigrations, embed_migrations}; #[macro_use] extern crate serde_derive; @@ -9,8 +9,8 @@ use std::env; use std::env::args; use std::process::exit; mod controllers; -use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::DBType; +use crate::adapters::persistence::dbconfig::db::get_connection; use crate::command_line_runner::start_command_line; use crate::commands::startup::handle_config_for_server_startup; use crate::constants::inner_constants::ENVIRONMENT_SERVICE; diff --git a/src/models/episode.rs b/src/models/episode.rs index d60e1a131..bf1a1b4df 100644 --- a/src/models/episode.rs +++ b/src/models/episode.rs @@ -1,3 +1,4 @@ +use crate::DBType as DbConnection; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::episodes; use crate::adapters::persistence::dbconfig::schema::episodes::dsl::episodes as episodes_dsl; @@ -11,8 +12,7 @@ use crate::models::podcast_episode::PodcastEpisode; use crate::models::podcasts::Podcast; use crate::models::user::User; use crate::utils::error::ErrorSeverity::{Critical, Warning}; -use crate::utils::error::{map_db_error, CustomError, CustomErrorInner}; -use crate::DBType as DbConnection; +use crate::utils::error::{CustomError, CustomErrorInner, map_db_error}; use chrono::{NaiveDateTime, Utc}; use diesel::query_dsl::methods::DistinctDsl; use diesel::sql_types::{Integer, Nullable, Text, Timestamp}; diff --git a/src/models/favorite_podcast_episode.rs b/src/models/favorite_podcast_episode.rs index 29b3d3541..85d8f4a56 100644 --- a/src/models/favorite_podcast_episode.rs +++ b/src/models/favorite_podcast_episode.rs @@ -2,7 +2,7 @@ use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::favorite_podcast_episodes; use crate::models::user::User; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use diesel::ExpressionMethods; use diesel::QueryDsl; use diesel::{Insertable, OptionalExtension, Queryable, QueryableByName, RunQueryDsl}; diff --git a/src/models/favorites.rs b/src/models/favorites.rs index 409c0e7de..ea98f4ef6 100644 --- a/src/models/favorites.rs +++ b/src/models/favorites.rs @@ -1,3 +1,4 @@ +use crate::DBType as DbConnection; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::favorites; use crate::models::order_criteria::{OrderCriteria, OrderOption}; @@ -7,8 +8,7 @@ use crate::models::podcasts::Podcast; use crate::models::tag::Tag; use crate::models::user::User; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; -use crate::DBType as DbConnection; +use crate::utils::error::{CustomError, map_db_error}; use diesel::insert_into; use diesel::prelude::*; use diesel::sql_types::{Bool, Integer, Text}; diff --git a/src/models/file_path.rs b/src/models/file_path.rs index 320c330bb..8b41a8435 100644 --- a/src/models/file_path.rs +++ b/src/models/file_path.rs @@ -1,10 +1,10 @@ +use crate::DBType as DbConnection; use crate::models::podcast_episode::PodcastEpisode; use crate::models::podcasts::Podcast; use crate::models::settings::Setting; use crate::service::file_service::prepare_podcast_episode_title_to_directory; use crate::service::path_service::PathService; use crate::utils::error::CustomError; -use crate::DBType as DbConnection; #[derive(Default, Clone, Debug)] pub struct FilenameBuilder { diff --git a/src/models/filter.rs b/src/models/filter.rs index b36b70545..7cb680782 100644 --- a/src/models/filter.rs +++ b/src/models/filter.rs @@ -2,7 +2,7 @@ use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::filters; use crate::execute_with_conn; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use diesel::AsChangeset; use diesel::ExpressionMethods; use diesel::QueryDsl; diff --git a/src/models/invite.rs b/src/models/invite.rs index b7718b973..766840d39 100644 --- a/src/models/invite.rs +++ b/src/models/invite.rs @@ -2,10 +2,10 @@ use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::invites; use crate::constants::inner_constants::Role; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use chrono::NaiveDateTime; -use diesel::associations::HasTable; use diesel::ExpressionMethods; +use diesel::associations::HasTable; use diesel::{Identifiable, Insertable, OptionalExtension, QueryDsl, Queryable, RunQueryDsl}; use std::io::Error; use utoipa::ToSchema; diff --git a/src/models/notification.rs b/src/models/notification.rs index c4babe91a..addd56e5a 100644 --- a/src/models/notification.rs +++ b/src/models/notification.rs @@ -1,9 +1,9 @@ use crate::adapters::persistence::dbconfig::db::get_connection; use crate::utils::do_retry::do_retry; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; -use diesel::insert_into; +use crate::utils::error::{CustomError, map_db_error}; use diesel::Queryable; +use diesel::insert_into; use utoipa::ToSchema; #[derive(Serialize, Deserialize, Queryable, Clone, ToSchema)] diff --git a/src/models/playlist.rs b/src/models/playlist.rs index 0b92790ff..ec900f300 100644 --- a/src/models/playlist.rs +++ b/src/models/playlist.rs @@ -9,12 +9,12 @@ use crate::models::playlist_item::PlaylistItem; use crate::models::podcast_episode::PodcastEpisode; use crate::models::user::User; use crate::utils::error::ErrorSeverity::{Critical, Debug, Info, Warning}; -use crate::utils::error::{map_db_error, CustomError, CustomErrorInner}; -use crate::{execute_with_conn, DBType as DbConnection}; -use diesel::prelude::*; -use diesel::sql_types::{Integer, Text}; +use crate::utils::error::{CustomError, CustomErrorInner, map_db_error}; +use crate::{DBType as DbConnection, execute_with_conn}; use diesel::ExpressionMethods; use diesel::RunQueryDsl; +use diesel::prelude::*; +use diesel::sql_types::{Integer, Text}; use diesel::{Queryable, QueryableByName}; use utoipa::ToSchema; use uuid::Uuid; diff --git a/src/models/playlist_item.rs b/src/models/playlist_item.rs index fd819009a..af0d8c837 100644 --- a/src/models/playlist_item.rs +++ b/src/models/playlist_item.rs @@ -1,15 +1,15 @@ +use crate::DBType as DbConnection; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::playlist_items; use crate::models::episode::Episode; use crate::models::podcast_episode::PodcastEpisode; use crate::models::user::User; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; -use crate::DBType as DbConnection; +use crate::utils::error::{CustomError, map_db_error}; +use diesel::ExpressionMethods; use diesel::dsl::max; use diesel::prelude::*; use diesel::sql_types::{Integer, Text}; -use diesel::ExpressionMethods; use diesel::{Queryable, QueryableByName}; use utoipa::ToSchema; diff --git a/src/models/podcast_episode.rs b/src/models/podcast_episode.rs index bf67decaa..bf8a05c1b 100644 --- a/src/models/podcast_episode.rs +++ b/src/models/podcast_episode.rs @@ -1,10 +1,11 @@ +use crate::DBType as DbConnection; use crate::adapters::file::file_handler::FileHandlerType; +use crate::adapters::persistence::dbconfig::DBType; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::favorite_podcast_episodes::dsl::favorite_podcast_episodes; use crate::adapters::persistence::dbconfig::schema::*; -use crate::adapters::persistence::dbconfig::DBType; use crate::constants::inner_constants::{ - PodcastEpisodeWithFavorited, DEFAULT_IMAGE_URL, ENVIRONMENT_SERVICE, + DEFAULT_IMAGE_URL, ENVIRONMENT_SERVICE, PodcastEpisodeWithFavorited, }; use crate::models::episode::Episode; use crate::models::favorite_podcast_episode::FavoritePodcastEpisode; @@ -13,20 +14,19 @@ use crate::models::podcasts::Podcast; use crate::models::user::User; use crate::utils::do_retry::do_retry; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use crate::utils::time::opt_or_empty_string; -use crate::DBType as DbConnection; use chrono::{DateTime, Duration, FixedOffset, NaiveDateTime, ParseResult, Utc}; -use diesel::dsl::{max, sql, IsNotNull}; -use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable}; -use diesel::query_source::Alias; -use diesel::sql_types::{Bool, Integer, Nullable, Text, Timestamp}; use diesel::AsChangeset; use diesel::ExpressionMethods; use diesel::QueryDsl; +use diesel::dsl::{IsNotNull, max, sql}; +use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable}; +use diesel::query_source::Alias; +use diesel::sql_types::{Bool, Integer, Nullable, Text, Timestamp}; use diesel::{ - delete, insert_into, BoolExpressionMethods, JoinOnDsl, NullableExpressionMethods, - OptionalExtension, RunQueryDsl, TextExpressionMethods, + BoolExpressionMethods, JoinOnDsl, NullableExpressionMethods, OptionalExtension, RunQueryDsl, + TextExpressionMethods, delete, insert_into, }; use rss::{Guid, Item}; use utoipa::ToSchema; @@ -228,7 +228,8 @@ impl PodcastEpisode { value: uuid::Uuid::new_v4().to_string(), ..Default::default() }; - let inserted_podcast = insert_into(podcast_episodes) + + insert_into(podcast_episodes) .values(( total_time.eq(duration), podcast_id.eq(podcast.id), @@ -241,9 +242,7 @@ impl PodcastEpisode { description.eq(opt_or_empty_string(item.description)), )) .get_result::(&mut get_connection()) - .expect("Error inserting podcast episode"); - - inserted_podcast + .expect("Error inserting podcast episode") } pub fn get_podcast_episodes_of_podcast( @@ -292,14 +291,14 @@ impl PodcastEpisode { podcast_query = podcast_query.filter(date_of_recording.lt(last_id)); } - if let Some(only_unlistened) = &only_unlistened { - if *only_unlistened { - podcast_query = podcast_query.filter( - ph1.field(phistory_position) - .is_null() - .or(ph1.field(phistory_total).ne(ph1.field(phistory_position))), - ); - } + if let Some(only_unlistened) = &only_unlistened + && *only_unlistened + { + podcast_query = podcast_query.filter( + ph1.field(phistory_position) + .is_null() + .or(ph1.field(phistory_total).ne(ph1.field(phistory_position))), + ); } podcast_query diff --git a/src/models/podcast_settings.rs b/src/models/podcast_settings.rs index 06a506b33..b7fab31cc 100644 --- a/src/models/podcast_settings.rs +++ b/src/models/podcast_settings.rs @@ -5,7 +5,7 @@ use crate::models::podcast_episode::PodcastEpisode; use crate::models::podcasts::Podcast; use crate::service::download_service::DownloadService; use crate::utils::error::ErrorSeverity::{Critical, Warning}; -use crate::utils::error::{map_db_error, CustomError, CustomErrorInner}; +use crate::utils::error::{CustomError, CustomErrorInner, map_db_error}; use diesel::{ AsChangeset, Identifiable, Insertable, OptionalExtension, QueryDsl, Queryable, RunQueryDsl, }; diff --git a/src/models/podcasts.rs b/src/models/podcasts.rs index e42286818..34aa0d28c 100644 --- a/src/models/podcasts.rs +++ b/src/models/podcasts.rs @@ -1,5 +1,6 @@ use crate::adapters::persistence::dbconfig::schema::*; +use crate::DBType as DbConnection; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::models::favorites::Favorite; use crate::models::podcast_dto::PodcastDto; @@ -7,17 +8,16 @@ use crate::models::podcast_episode::PodcastEpisode; use crate::models::tag::Tag; use crate::utils::do_retry::do_retry; use crate::utils::podcast_builder::PodcastExtra; -use crate::DBType as DbConnection; -use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable}; -use diesel::sql_types::{Bool, Integer, Nullable, Text}; use diesel::ExpressionMethods; use diesel::QueryDsl; +use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable}; +use diesel::sql_types::{Bool, Integer, Nullable, Text}; use diesel::{ - delete, insert_into, BoolExpressionMethods, JoinOnDsl, OptionalExtension, RunQueryDsl, + BoolExpressionMethods, JoinOnDsl, OptionalExtension, RunQueryDsl, delete, insert_into, }; use crate::utils::error::ErrorSeverity::{Critical, Warning}; -use crate::utils::error::{map_db_error, CustomError, CustomErrorInner}; +use crate::utils::error::{CustomError, CustomErrorInner, map_db_error}; #[derive( Queryable, Identifiable, QueryableByName, Selectable, Debug, PartialEq, Clone, Default, diff --git a/src/models/session.rs b/src/models/session.rs index 463e82fd5..ed9cf6257 100644 --- a/src/models/session.rs +++ b/src/models/session.rs @@ -1,8 +1,8 @@ use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::sessions; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; -use crate::{execute_with_conn, DBType as DbConnection}; +use crate::utils::error::{CustomError, map_db_error}; +use crate::{DBType as DbConnection, execute_with_conn}; use chrono::{DateTime, NaiveDateTime, Utc}; use diesel::ExpressionMethods; use diesel::QueryDsl; diff --git a/src/models/settings.rs b/src/models/settings.rs index cdeb84250..c45aa783b 100644 --- a/src/models/settings.rs +++ b/src/models/settings.rs @@ -4,7 +4,7 @@ use crate::constants::inner_constants::DEFAULT_SETTINGS; use crate::service::environment_service::OidcConfig; use crate::utils::do_retry::do_retry; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use diesel::insert_into; use diesel::prelude::{AsChangeset, Identifiable, Insertable, Queryable}; use diesel::{OptionalExtension, RunQueryDsl}; diff --git a/src/models/subscription.rs b/src/models/subscription.rs index c3bf79fd8..cd7115185 100644 --- a/src/models/subscription.rs +++ b/src/models/subscription.rs @@ -1,15 +1,15 @@ +use crate::DBType as DbConnection; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::subscriptions; use crate::gpodder::subscription::subscriptions::SubscriptionUpdateRequest; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use crate::utils::time::get_current_timestamp; -use crate::DBType as DbConnection; use axum::Json; use chrono::{DateTime, NaiveDateTime, Utc}; -use diesel::sql_types::{Integer, Nullable, Text, Timestamp}; use diesel::ExpressionMethods; use diesel::OptionalExtension; +use diesel::sql_types::{Integer, Nullable, Text, Timestamp}; use diesel::{AsChangeset, Insertable, Queryable, QueryableByName}; use diesel::{BoolExpressionMethods, QueryDsl, RunQueryDsl}; use serde::{Deserialize, Serialize}; diff --git a/src/models/tag.rs b/src/models/tag.rs index a7aff9799..5f010c452 100644 --- a/src/models/tag.rs +++ b/src/models/tag.rs @@ -2,7 +2,7 @@ use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::tags; use crate::execute_with_conn; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use chrono::{NaiveDateTime, Utc}; use diesel::sql_types::{Nullable, Text, Timestamp}; use diesel::{ diff --git a/src/models/tags_podcast.rs b/src/models/tags_podcast.rs index 37fae9d81..c0e52c695 100644 --- a/src/models/tags_podcast.rs +++ b/src/models/tags_podcast.rs @@ -1,6 +1,6 @@ use crate::adapters::persistence::dbconfig::schema::tags_podcasts; use crate::utils::error::ErrorSeverity::Critical; -use crate::utils::error::{map_db_error, CustomError}; +use crate::utils::error::{CustomError, map_db_error}; use crate::{execute_with_conn, insert_with_conn}; use diesel::{AsChangeset, Insertable, Queryable, QueryableByName}; use utoipa::ToSchema; diff --git a/src/models/user.rs b/src/models/user.rs index 82d2991e2..f5a791243 100644 --- a/src/models/user.rs +++ b/src/models/user.rs @@ -1,18 +1,18 @@ +use crate::DBType as DbConnection; use crate::adapters::persistence::dbconfig::db::get_connection; use crate::adapters::persistence::dbconfig::schema::users; use crate::constants::inner_constants::{ - Role, BASIC_AUTH, ENVIRONMENT_SERVICE, OIDC_AUTH, STANDARD_USER, USERNAME, + BASIC_AUTH, ENVIRONMENT_SERVICE, OIDC_AUTH, Role, STANDARD_USER, STANDARD_USER_ID, USERNAME, }; use crate::utils::environment_variables::is_env_var_present_and_true; use crate::utils::error::ErrorSeverity::{Critical, Debug, Info, Warning}; -use crate::utils::error::{map_db_error, CustomError, CustomErrorInner}; -use crate::DBType as DbConnection; +use crate::utils::error::{CustomError, CustomErrorInner, map_db_error}; use axum::extract::Request; use chrono::NaiveDateTime; -use diesel::associations::HasTable; -use diesel::prelude::{Insertable, Queryable}; use diesel::ExpressionMethods; use diesel::QueryDsl; +use diesel::associations::HasTable; +use diesel::prelude::{Insertable, Queryable}; use diesel::{AsChangeset, OptionalExtension, RunQueryDsl}; use std::io::Error; use utoipa::ToSchema; @@ -48,6 +48,7 @@ pub struct UserWithAPiKey { pub created_at: NaiveDateTime, pub explicit_consent: bool, pub api_key: Option, + pub read_only: bool, } impl User { @@ -72,11 +73,11 @@ impl User { pub fn find_by_username(username_to_find: &str) -> Result { use crate::adapters::persistence::dbconfig::schema::users::dsl::*; - if let Some(res) = ENVIRONMENT_SERVICE.username.clone() { - if res == username_to_find { - let admin = User::create_admin_user(); - return Ok(admin); - } + if let Some(res) = ENVIRONMENT_SERVICE.username.clone() + && res == username_to_find + { + let admin = User::create_admin_user(); + return Ok(admin); } let opt_user = users @@ -93,10 +94,10 @@ impl User { pub fn insert_user(&mut self) -> Result { use crate::adapters::persistence::dbconfig::schema::users::dsl::*; - if let Some(res) = ENVIRONMENT_SERVICE.username.clone() { - if res == self.username { - return Err(CustomErrorInner::Forbidden(Warning).into()); - } + if let Some(res) = ENVIRONMENT_SERVICE.username.clone() + && res == self.username + { + return Err(CustomErrorInner::Forbidden(Warning).into()); } let res = diesel::insert_into(users::table()) @@ -137,7 +138,7 @@ impl User { let password: Option = ENVIRONMENT_SERVICE.password.clone(); let username = ENVIRONMENT_SERVICE.username.clone(); User { - id: 9999, + id: STANDARD_USER_ID, username: username.unwrap_or(STANDARD_USER.to_string()), role: Role::Admin.to_string(), password, @@ -158,14 +159,20 @@ impl User { } pub fn map_to_api_dto(user: Self) -> UserWithAPiKey { - UserWithAPiKey { + let mut user_with_api_key = UserWithAPiKey { id: user.id, explicit_consent: user.explicit_consent, username: user.username.clone(), role: user.role.clone(), created_at: user.created_at, api_key: user.api_key.clone(), + read_only: false, + }; + if user.id == Self::create_standard_admin_user().id { + user_with_api_key.read_only = true; } + + user_with_api_key } pub fn create_standard_admin_user() -> User { @@ -308,10 +315,11 @@ impl User { return false; } - if let Some(res) = ENVIRONMENT_SERVICE.api_key_admin.clone() { - if !res.is_empty() && res == api_key_to_find { - return true; - } + if let Some(res) = ENVIRONMENT_SERVICE.api_key_admin.clone() + && !res.is_empty() + && res == api_key_to_find + { + return true; } let result = Self::find_by_api_key(api_key_to_find); diff --git a/src/service/download_service.rs b/src/service/download_service.rs index 7ff2de461..c7c89c485 100644 --- a/src/service/download_service.rs +++ b/src/service/download_service.rs @@ -13,9 +13,9 @@ use crate::models::file_path::{FilenameBuilder, FilenameBuilderReturn}; use crate::models::podcast_settings::PodcastSetting; use crate::models::settings::Setting; use crate::service::podcast_episode_service::PodcastEpisodeService; -use crate::utils::error::{map_reqwest_error, CustomError, CustomErrorInner, ErrorSeverity}; +use crate::utils::error::{CustomError, CustomErrorInner, ErrorSeverity, map_reqwest_error}; use crate::utils::file_extension_determination::{ - determine_file_extension, DetermineFileExtensionReturn, FileType, + DetermineFileExtensionReturn, FileType, determine_file_extension, }; use crate::utils::http_client::get_async_sync_client; use crate::utils::reqwest_client::get_sync_client; @@ -123,17 +123,17 @@ impl DownloadService { )?; } - if let Some(p) = PathBuf::from(&paths.filename).parent() { - if !FileHandleWrapper::path_exists( + if let Some(p) = PathBuf::from(&paths.filename).parent() + && !FileHandleWrapper::path_exists( p.to_str().unwrap(), FileRequest::Directory, &ENVIRONMENT_SERVICE.default_file_handler, - ) { - FileHandleWrapper::create_dir( - p.to_str().unwrap(), - &ENVIRONMENT_SERVICE.default_file_handler, - )?; - } + ) + { + FileHandleWrapper::create_dir( + p.to_str().unwrap(), + &ENVIRONMENT_SERVICE.default_file_handler, + )?; } if !FileService::check_if_podcast_main_image_downloaded(&podcast.clone().directory_id, conn) @@ -222,7 +222,7 @@ impl DownloadService { Err(err) => { return Err( CustomErrorInner::Conflict(err.to_string(), ErrorSeverity::Error).into(), - ) + ); } }; @@ -278,10 +278,10 @@ impl DownloadService { ) } - if tag.artist().is_none() { - if let Some(author) = &podcast.author { - tag.set_artist(author); - } + if tag.artist().is_none() + && let Some(author) = &podcast.author + { + tag.set_artist(author); } if tag.album().is_none() { @@ -290,10 +290,10 @@ impl DownloadService { tag.set_date_recorded(podcast_episode.date_of_recording.parse().unwrap()); - if tag.genres().is_none() { - if let Some(keywords) = &podcast.keywords { - tag.set_genre(keywords); - } + if tag.genres().is_none() + && let Some(keywords) = &podcast.keywords + { + tag.set_genre(keywords); } if tag.clone().comments().next().is_none() { @@ -309,10 +309,10 @@ impl DownloadService { &podcast_episode.date_of_recording, ); - if tag.track().is_none() { - if let Ok(track_number) = track_number { - tag.set_track(track_number as u32); - } + if tag.track().is_none() + && let Ok(track_number) = track_number + { + tag.set_track(track_number as u32); } let write_succesful: Result<(), CustomError> = tag diff --git a/src/service/environment_service.rs b/src/service/environment_service.rs index 9fa34d939..e216d553f 100644 --- a/src/service/environment_service.rs +++ b/src/service/environment_service.rs @@ -390,18 +390,20 @@ mod tests { use std::env::{remove_var, set_var}; fn do_env_cleanup() { - remove_var(SERVER_URL); - remove_var(PODINDEX_API_KEY); - remove_var(PODINDEX_API_SECRET); - remove_var(POLLING_INTERVAL); - remove_var(BASIC_AUTH); - remove_var(USERNAME); - remove_var(PASSWORD); - remove_var(OIDC_AUTH); - remove_var(OIDC_REDIRECT_URI); - remove_var(OIDC_AUTHORITY); - remove_var(OIDC_CLIENT_ID); - remove_var(OIDC_SCOPE); + unsafe { + remove_var(SERVER_URL); + remove_var(PODINDEX_API_KEY); + remove_var(PODINDEX_API_SECRET); + remove_var(POLLING_INTERVAL); + remove_var(BASIC_AUTH); + remove_var(USERNAME); + remove_var(PASSWORD); + remove_var(OIDC_AUTH); + remove_var(OIDC_REDIRECT_URI); + remove_var(OIDC_AUTHORITY); + remove_var(OIDC_CLIENT_ID); + remove_var(OIDC_SCOPE); + } } #[test] @@ -409,17 +411,20 @@ mod tests { fn test_get_config() { do_env_cleanup(); - set_var(SERVER_URL, "http://localhost:8000"); - set_var(POLLING_INTERVAL, "10"); - set_var(BASIC_AUTH, "true"); - set_var(USERNAME, "test"); - set_var(PASSWORD, "test"); - set_var(OIDC_AUTH, "true"); - set_var(OIDC_REDIRECT_URI, "http://localhost:8000/oidc"); - set_var(OIDC_AUTHORITY, "http://localhost:8000/oidc"); - set_var(OIDC_CLIENT_ID, "test"); - set_var(OIDC_SCOPE, "openid profile email"); - set_var(OIDC_JWKS, "test"); + unsafe { + set_var(SERVER_URL, "http://localhost:8000"); + set_var(POLLING_INTERVAL, "10"); + set_var(BASIC_AUTH, "true"); + set_var(USERNAME, "test"); + set_var(PASSWORD, "test"); + set_var(OIDC_AUTH, "true"); + set_var(OIDC_REDIRECT_URI, "http://localhost:8000/oidc"); + set_var(OIDC_AUTHORITY, "http://localhost:8000/oidc"); + set_var(OIDC_CLIENT_ID, "test"); + set_var(OIDC_SCOPE, "openid profile email"); + set_var(OIDC_JWKS, "test"); + } + let env_service = EnvironmentService::new(); let config = env_service.get_config(); assert!(!config.podindex_configured); @@ -445,7 +450,9 @@ mod tests { #[serial] fn test_getting_server_url() { do_env_cleanup(); - set_var(SERVER_URL, "http://localhost:8000"); + unsafe { + set_var(SERVER_URL, "http://localhost:8000"); + } let env_service = EnvironmentService::new(); assert_eq!(env_service.get_server_url(), "http://localhost:8000/"); @@ -455,13 +462,16 @@ mod tests { #[serial] fn test_get_config_without_oidc() { do_env_cleanup(); - set_var(SERVER_URL, "http://localhost:8000"); - set_var(PODINDEX_API_KEY, "test"); - set_var(PODINDEX_API_SECRET, "test"); - set_var(POLLING_INTERVAL, "10"); - set_var(BASIC_AUTH, "true"); - set_var(USERNAME, "test"); - set_var(PASSWORD, "test"); + unsafe { + set_var(SERVER_URL, "http://localhost:8000"); + set_var(PODINDEX_API_KEY, "test"); + set_var(PODINDEX_API_SECRET, "test"); + set_var(POLLING_INTERVAL, "10"); + set_var(BASIC_AUTH, "true"); + set_var(USERNAME, "test"); + set_var(PASSWORD, "test"); + } + let config = EnvironmentService::new().get_config(); assert!(config.podindex_configured); assert_eq!(config.rss_feed, "http://localhost:8000/rss"); @@ -474,9 +484,10 @@ mod tests { #[serial] fn test_get_podindex_api_key() { do_env_cleanup(); - set_var(PODINDEX_API_KEY, "test"); - set_var(PODINDEX_API_SECRET, "testsecret"); - + unsafe { + set_var(PODINDEX_API_KEY, "test"); + set_var(PODINDEX_API_SECRET, "testsecret"); + } let env_service = EnvironmentService::new(); assert_eq!(env_service.podindex_api_key, "test"); assert_eq!(env_service.podindex_api_secret, "testsecret"); @@ -486,7 +497,9 @@ mod tests { #[serial] fn test_get_polling_interval() { do_env_cleanup(); - set_var(POLLING_INTERVAL, "20"); + unsafe { + set_var(POLLING_INTERVAL, "20"); + } assert_eq!(EnvironmentService::new().polling_interval, 20); } } diff --git a/src/service/file_service.rs b/src/service/file_service.rs index b821bd04d..be4e17042 100644 --- a/src/service/file_service.rs +++ b/src/service/file_service.rs @@ -5,6 +5,7 @@ use crate::models::podcasts::Podcast; use std::path::Path; use std::str::FromStr; +use crate::DBType as DbConnection; use crate::adapters::file::file_handle_wrapper::FileHandleWrapper; use crate::adapters::file::file_handler::{FileHandlerType, FileRequest}; use crate::adapters::persistence::dbconfig::db::get_connection; @@ -18,10 +19,9 @@ use crate::service::download_service::DownloadService; use crate::service::path_service::PathService; use crate::service::settings_service::SettingsService; use crate::utils::error::{CustomError, CustomErrorInner, ErrorSeverity}; -use crate::utils::file_extension_determination::{determine_file_extension, FileType}; +use crate::utils::file_extension_determination::{FileType, determine_file_extension}; use crate::utils::file_name_replacement::{Options, Sanitizer}; use crate::utils::rss_feed_parser::RSSFeedParser; -use crate::DBType as DbConnection; use regex::Regex; use rss::Channel; use tokio::task::spawn_blocking; diff --git a/src/service/path_service.rs b/src/service/path_service.rs index 101009509..853f4dca3 100644 --- a/src/service/path_service.rs +++ b/src/service/path_service.rs @@ -1,3 +1,4 @@ +use crate::DBType as DbConnection; use crate::adapters::file::file_handle_wrapper::FileHandleWrapper; use crate::adapters::file::file_handler::{FileHandlerType, FileRequest}; use crate::models::podcast_episode::PodcastEpisode; @@ -5,7 +6,6 @@ use crate::models::podcasts::Podcast; use crate::service::file_service::prepare_podcast_episode_title_to_directory; use crate::service::podcast_episode_service::PodcastEpisodeService; use crate::utils::error::CustomError; -use crate::DBType as DbConnection; pub struct PathService {} diff --git a/src/service/podcast_episode_service.rs b/src/service/podcast_episode_service.rs index dd4fbf6f8..b683c1318 100644 --- a/src/service/podcast_episode_service.rs +++ b/src/service/podcast_episode_service.rs @@ -18,14 +18,14 @@ use crate::service::telegram_api::send_new_episode_notification; use crate::utils::environment_variables::is_env_var_present_and_true; use crate::utils::error::ErrorSeverity::{Critical, Warning}; use crate::utils::error::{ - map_db_error, map_reqwest_error, CustomError, CustomErrorInner, ErrorSeverity, + CustomError, CustomErrorInner, ErrorSeverity, map_db_error, map_reqwest_error, }; use crate::utils::podcast_builder::PodcastBuilder; use crate::utils::reqwest_client::get_sync_client; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl}; use log::error; use regex::Regex; -use reqwest::header::{HeaderMap, ACCEPT}; +use reqwest::header::{ACCEPT, HeaderMap}; use reqwest::redirect::Policy; use rss::{Channel, Item}; use std::io::Error; @@ -162,18 +162,16 @@ impl PodcastEpisodeService { let result_unwrapped = result.unwrap(); if let Some(result_unwrapped_non_opt) = result_unwrapped.clone() + && result_unwrapped_non_opt.clone().podcast_id != podcast.id { - if result_unwrapped_non_opt.clone().podcast_id != podcast.id - { - let inserted_episode = - PodcastEpisode::insert_podcast_episodes( - podcast.clone(), - item.clone(), - Some(result_unwrapped_non_opt.image_url), - duration_episode as i32, - ); - podcast_inserted.push(inserted_episode); - } + let inserted_episode = + PodcastEpisode::insert_podcast_episodes( + podcast.clone(), + item.clone(), + Some(result_unwrapped_non_opt.image_url), + duration_episode as i32, + ); + podcast_inserted.push(inserted_episode); } if result_unwrapped.is_none() { @@ -257,14 +255,12 @@ impl PodcastEpisodeService { } fn extract_itunes_url_if_present(item: &Item) -> Option { - if let Some(itunes_data) = item.extensions.get(ITUNES) { - if let Some(image_url_extracted) = itunes_data.get("image") { - if let Some(i_val) = image_url_extracted.first() { - if let Some(image_attr) = i_val.attrs.get("href") { - return Some(image_attr.clone()); - } - } - } + if let Some(itunes_data) = item.extensions.get(ITUNES) + && let Some(image_url_extracted) = itunes_data.get("image") + && let Some(i_val) = image_url_extracted.first() + && let Some(image_attr) = i_val.attrs.get("href") + { + return Some(image_attr.clone()); } None } diff --git a/src/service/rust_service.rs b/src/service/rust_service.rs index 4bed6ddcd..436b1d1be 100644 --- a/src/service/rust_service.rs +++ b/src/service/rust_service.rs @@ -15,7 +15,7 @@ use crate::service::file_service::FileService; use crate::service::podcast_episode_service::PodcastEpisodeService; use crate::unwrap_string; use crate::utils::error::ErrorSeverity::{Critical, Error}; -use crate::utils::error::{map_reqwest_error, CustomError, CustomErrorInner, ErrorSeverity}; +use crate::utils::error::{CustomError, CustomErrorInner, ErrorSeverity, map_reqwest_error}; use crate::utils::http_client::get_http_client; use reqwest::header::{HeaderMap, HeaderValue}; use rss::Channel; @@ -183,15 +183,14 @@ impl PodcastService { let result = PodcastEpisodeService::get_last_n_podcast_episodes(podcast.clone())?; for podcast_episode in result { - if !podcast_episode.deleted { - if let Err(e) = + if !podcast_episode.deleted + && let Err(e) = PodcastEpisodeService::download_podcast_episode_if_not_locally_available( podcast_episode, podcast.clone(), ){ log::error!("Error downloading podcast episode: {e}"); } - } } } Ok(()) diff --git a/src/service/user_management_service.rs b/src/service/user_management_service.rs index d81d2359f..d3e036c28 100644 --- a/src/service/user_management_service.rs +++ b/src/service/user_management_service.rs @@ -1,5 +1,5 @@ use crate::adapters::persistence::dbconfig::db::get_connection; -use crate::constants::inner_constants::{Role, ENVIRONMENT_SERVICE}; +use crate::constants::inner_constants::{ENVIRONMENT_SERVICE, Role}; use crate::models::invite::Invite; use crate::models::user::{User, UserWithoutPassword}; use crate::utils::error::ErrorSeverity::{Critical, Debug, Error, Info, Warning}; diff --git a/src/utils/append_to_header.rs b/src/utils/append_to_header.rs index 63dac7e38..6b5e63b73 100644 --- a/src/utils/append_to_header.rs +++ b/src/utils/append_to_header.rs @@ -1,5 +1,5 @@ -use base64::engine::general_purpose; use base64::Engine; +use base64::engine::general_purpose; use regex::Regex; use std::process::exit; use std::sync::LazyLock; @@ -16,15 +16,14 @@ pub fn add_basic_auth_headers_conditionally( url: String, header_map: &mut reqwest::header::HeaderMap, ) { - if url.contains('@') { - if let Some(captures) = BASIC_AUTH_COND_REGEX.captures(&url) { - if let Some(auth) = captures.get(1) { - let b64_auth = general_purpose::STANDARD.encode(auth.as_str()); - let mut bearer = "Basic ".to_owned(); - bearer.push_str(&b64_auth); - header_map.append("Authorization", bearer.parse().unwrap()); - } - } + if url.contains('@') + && let Some(captures) = BASIC_AUTH_COND_REGEX.captures(&url) + && let Some(auth) = captures.get(1) + { + let b64_auth = general_purpose::STANDARD.encode(auth.as_str()); + let mut bearer = "Basic ".to_owned(); + bearer.push_str(&b64_auth); + header_map.append("Authorization", bearer.parse().unwrap()); } } diff --git a/src/utils/auth.rs b/src/utils/auth.rs index 127f193ea..2d6ca362d 100644 --- a/src/utils/auth.rs +++ b/src/utils/auth.rs @@ -2,8 +2,8 @@ pub mod tests { use crate::commands::startup::tests::TestServerWrapper; use crate::models::user::User; - use base64::engine::general_purpose; use base64::Engine; + use base64::engine::general_purpose; pub fn create_basic_header(username: &str, password: &str) -> String { general_purpose::STANDARD.encode(format!("{username}:{password}")) diff --git a/src/utils/error.rs b/src/utils/error.rs index 98b6a5b4f..2a7881fd4 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -3,7 +3,8 @@ use axum::response::{IntoResponse, Response}; use log::{debug, error, info, warn}; use s3::error::S3Error; use std::backtrace::Backtrace; -use std::convert::Infallible; +use std::collections::HashMap; +use std::convert::{Infallible, Into}; use std::error::Error; use std::fmt::{Debug, Display}; use std::ops::{Deref, DerefMut}; @@ -15,6 +16,68 @@ pub struct CustomError { pub error_severity: ErrorSeverity, } +pub struct ApiError { + pub status: StatusCode, + pub value: ApiErrorValue, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApiErrorValue { + pub error_code: String, + pub arguments: HashMap, +} + +impl ApiError { + pub fn updating_admin_not_allowed(username: &str) -> Self { + let mut args = HashMap::new(); + args.insert("username".into(), username.into()); + ApiError { + value: ApiErrorValue { + error_code: "UPDATE_OF_ADMIN_NOT_ALLOWED".into(), + arguments: args.clone(), + }, + status: StatusCode::BAD_REQUEST, + } + } +} + +pub enum ErrorType { + CustomErrorType(CustomError), + ApiErrorType(ApiError), +} + +impl IntoResponse for ErrorType { + fn into_response(self) -> Response { + match self { + ErrorType::CustomErrorType(ce) => ce.into_response(), + ErrorType::ApiErrorType(ae) => { + let body = serde_json::to_string(&ae.value) + .unwrap_or_else(|_| "{\"error\":\"Serialization error\"}".to_string()); + (ae.status, body).into_response() + } + } + } +} + +impl From for ErrorType { + fn from(value: CustomError) -> Self { + ErrorType::CustomErrorType(value) + } +} + +impl From for ErrorType { + fn from(value: CustomErrorInner) -> Self { + ErrorType::CustomErrorType(value.into()) + } +} + +impl From for ErrorType { + fn from(value: ApiError) -> Self { + ErrorType::ApiErrorType(value) + } +} + impl Display for CustomError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "Initial error: {:}", self.inner)?; @@ -318,7 +381,7 @@ struct ErrorResponse { #[cfg(test)] mod tests { - use crate::utils::error::{map_db_error, map_io_error, CustomErrorInner, ErrorSeverity}; + use crate::utils::error::{CustomErrorInner, ErrorSeverity, map_db_error, map_io_error}; use diesel::result::Error; use serial_test::serial; @@ -329,9 +392,11 @@ mod tests { fn test_map_io_error() { let io_error = std::io::Error::new(ErrorKind::NotFound, "File not found"); let custom_error = map_io_error(io_error, None, ErrorSeverity::Error); - assert!(custom_error - .to_string() - .contains("Requested file was not found")); + assert!( + custom_error + .to_string() + .contains("Requested file was not found") + ); } #[test] diff --git a/src/utils/podcast_key_checker.rs b/src/utils/podcast_key_checker.rs index 93802e9a7..7b1a1ae43 100644 --- a/src/utils/podcast_key_checker.rs +++ b/src/utils/podcast_key_checker.rs @@ -42,12 +42,12 @@ fn retrieve_podcast_or_podcast_episode( )); } - if let Some(image) = &podcast_episode.file_image_path { - if image.eq(path) { - return Ok(PodcastOrPodcastEpisodeResource::PodcastEpisode( - podcast_episode, - )); - } + if let Some(image) = &podcast_episode.file_image_path + && image.eq(path) + { + return Ok(PodcastOrPodcastEpisodeResource::PodcastEpisode( + podcast_episode, + )); } Ok(PodcastOrPodcastEpisodeResource::PodcastEpisode( @@ -77,7 +77,7 @@ fn check_auth( "No query parameters found".to_string(), Info, ) - .into()) + .into()); } }; diff --git a/src/utils/reqwest_client.rs b/src/utils/reqwest_client.rs index 662c85778..4ec3570e9 100644 --- a/src/utils/reqwest_client.rs +++ b/src/utils/reqwest_client.rs @@ -1,7 +1,7 @@ use crate::constants::inner_constants::{COMMON_USER_AGENT, ENVIRONMENT_SERVICE}; +use reqwest::Proxy; use reqwest::blocking::ClientBuilder; use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::Proxy; pub fn get_sync_client() -> ClientBuilder { let mut res = ClientBuilder::new(); diff --git a/src/utils/test_builder/device_test_builder.rs b/src/utils/test_builder/device_test_builder.rs index fcbfc336a..2593ba1d2 100644 --- a/src/utils/test_builder/device_test_builder.rs +++ b/src/utils/test_builder/device_test_builder.rs @@ -1,8 +1,8 @@ #[cfg(test)] pub mod tests { use crate::gpodder::device::dto::device_post::DevicePost; - use fake::faker::lorem::en::Word; use fake::Fake; + use fake::faker::lorem::en::Word; pub struct DevicePostTestDataBuilder { caption: String, diff --git a/src/utils/test_builder/podcast_test_builder.rs b/src/utils/test_builder/podcast_test_builder.rs index e5575ac99..a2da7cf3c 100644 --- a/src/utils/test_builder/podcast_test_builder.rs +++ b/src/utils/test_builder/podcast_test_builder.rs @@ -2,9 +2,9 @@ pub mod tests { use crate::models::podcasts::Podcast; use derive_builder::Builder; - use fake::faker::lorem::de_de::Word; use fake::Fake; use fake::Faker; + use fake::faker::lorem::de_de::Word; #[derive(Default, Builder, Debug)] #[builder(setter(into), default)] diff --git a/src/utils/test_builder/user_test_builder.rs b/src/utils/test_builder/user_test_builder.rs index c8e3adffe..2b3125469 100644 --- a/src/utils/test_builder/user_test_builder.rs +++ b/src/utils/test_builder/user_test_builder.rs @@ -1,9 +1,9 @@ #[cfg(test)] pub mod tests { use crate::models::user::User; + use fake::Fake; use fake::faker::internet::raw::Username; use fake::locales::EN; - use fake::Fake; pub struct UserTestDataBuilder { id: i32, diff --git a/ui/package.json b/ui/package.json index c225bcb56..9b67155a5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,9 @@ "build": "tsc && vite build", "preview": "vite preview", "build-github": "tsc && vite build --emptyOutDir --outDir ../static", + "tsc:watch": "tsc --watch --noEmit", + "tsc:check": "tsc --noEmit", + "test": "vitest", "generate:types": "npx openapi-typescript http://localhost:8000/api-docs/openapi.json -o ./schema.d.ts" }, "dependencies": { @@ -28,6 +31,8 @@ "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-toggle-group": "^1.1.11", + "@rsbuild/plugin-type-check": "^1.2.4", + "@tanstack/react-query": "^5.89.0", "@types/uuid": "^10.0.0", "chart.js": "^4.5.0", "copy-text-to-clipboard": "^3.2.1", @@ -39,8 +44,9 @@ "material-symbols": "^0.35.2", "notistack": "^3.0.2", "oidc-client-ts": "^3.3.0", - "react": "^19.1.1", "openapi-fetch": "^0.14.0", + "openapi-react-query": "^0.5.0", + "react": "^19.1.1", "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.1", "react-hook-form": "^7.62.0", @@ -71,7 +77,7 @@ "postcss": "^8.5.6", "tailwindcss": "^4.1.13", "typescript": "^5.9.2", - "vite": "npm:rolldown-vite@latest", + "vite": "npm:rolldown-vite@7.1.11", "vitest": "^3.2.4" } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 4be899fa3..84f156c57 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -47,6 +47,12 @@ importers: '@radix-ui/react-toggle-group': specifier: ^1.1.11 version: 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.13))(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@rsbuild/plugin-type-check': + specifier: ^1.2.4 + version: 1.2.4(@rsbuild/core@1.5.6)(@rspack/core@1.5.3(@swc/helpers@0.5.17))(typescript@5.9.2) + '@tanstack/react-query': + specifier: ^5.89.0 + version: 5.89.0(react@19.1.1) '@types/uuid': specifier: ^10.0.0 version: 10.0.0 @@ -83,6 +89,9 @@ importers: openapi-fetch: specifier: ^0.14.0 version: 0.14.0 + openapi-react-query: + specifier: ^0.5.0 + version: 0.5.0(@tanstack/react-query@5.89.0(react@19.1.1))(openapi-fetch@0.14.0) react: specifier: ^19.1.1 version: 19.1.1 @@ -152,7 +161,7 @@ importers: version: 2.16.0 '@vitejs/plugin-react': specifier: ^5.0.2 - version: 5.0.2(rolldown-vite@6.3.21(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2)) + version: 5.0.2(rolldown-vite@7.1.11(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2)) babel-plugin-react-compiler: specifier: 19.0.0-beta-30d8a17-20250209 version: 19.0.0-beta-30d8a17-20250209 @@ -172,8 +181,8 @@ importers: specifier: ^5.9.2 version: 5.9.2 vite: - specifier: npm:rolldown-vite@latest - version: rolldown-vite@6.3.21(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2) + specifier: npm:rolldown-vite@7.1.11 + version: rolldown-vite@7.1.11(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@24.4.0)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2) @@ -405,21 +414,12 @@ packages: resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} engines: {node: '>=18'} - '@emnapi/core@1.4.3': - resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} - '@emnapi/core@1.5.0': resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} - '@emnapi/runtime@1.4.3': - resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} - '@emnapi/runtime@1.5.0': resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@emnapi/wasi-threads@1.0.2': - resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} - '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -639,6 +639,42 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@1.0.0': + resolution: {integrity: sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.14.0': + resolution: {integrity: sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} @@ -660,18 +696,15 @@ packages: '@module-federation/webpack-bundler-runtime@0.18.0': resolution: {integrity: sha512-TEvErbF+YQ+6IFimhUYKK3a5wapD90d90sLsNpcu2kB3QGT7t4nIluE25duXuZDVUKLz86tEPrza/oaaCWTpvQ==} - '@napi-rs/wasm-runtime@0.2.11': - resolution: {integrity: sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==} - '@napi-rs/wasm-runtime@1.0.5': resolution: {integrity: sha512-TBr9Cf9onSAS2LQ2+QHx6XcC6h9+RIzJgbqG3++9TUZSH204AwEy5jg3BTQ0VATsyoGj4ee49tN/y6rvaOOtcg==} - '@oxc-project/runtime@0.73.0': - resolution: {integrity: sha512-YFvBzVQK/ix0RQxOI02ebCumehSHoiJgvb7nOU4o7xFoMnnujLdjmxnEBK/qiOQrEyXlY69gXGMEsKYVe+YZ3A==} + '@oxc-project/runtime@0.89.0': + resolution: {integrity: sha512-vP7SaoF0l09GAYuj4IKjfyJodRWC09KdLy8NmnsdUPAsWhPz+2hPTLfEr5+iObDXSNug1xfTxtkGjBLvtwBOPQ==} engines: {node: '>=6.9.0'} - '@oxc-project/types@0.73.0': - resolution: {integrity: sha512-ZQS7dpsga43R7bjqRKHRhOeNpuIBeLBnlS3M6H3IqWIWiapGOQIxp4lpETLBYupkSd4dh85ESFn6vAvtpPdGkA==} + '@oxc-project/types@0.89.0': + resolution: {integrity: sha512-yuo+ECPIW5Q9mSeNmCDC2im33bfKuwW18mwkaHMQh8KakHYDzj4ci/q7wxf2qS3dMlVVCIyrs3kFtH5LmnlYnw==} '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} @@ -1160,72 +1193,95 @@ packages: resolution: {integrity: sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==} engines: {node: '>=18.17.0', npm: '>=9.5.0'} - '@rolldown/binding-darwin-arm64@1.0.0-beta.16': - resolution: {integrity: sha512-dzlvuodUFc/QX97jYSsPHtYysqeSeM5gBxiN+DpV93tXEYyFMWm3cECxNmShz4ZM+lrgm6eG2/txzLZ/z9qWLw==} + '@rolldown/binding-android-arm64@1.0.0-beta.38': + resolution: {integrity: sha512-AE3HFQrjWCKLFZD1Vpiy+qsqTRwwoil1oM5WsKPSmfQ5fif/A+ZtOZetF32erZdsR7qyvns6qHEteEsF6g6rsQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-beta.38': + resolution: {integrity: sha512-RaoWOKc0rrFsVmKOjQpebMY6c6/I7GR1FBc25v7L/R7NlM0166mUotwGEv7vxu7ruXH4SJcFeVrfADFUUXUmmQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.16': - resolution: {integrity: sha512-H5604ucjaYy5AxxuOP/CoE5RV3lKCJ+btclWL5rV+hVh0qNN9dVgve+onzAYmi8h2RBPET1Novj+2KB640PC9Q==} + '@rolldown/binding-darwin-x64@1.0.0-beta.38': + resolution: {integrity: sha512-Ymojqc2U35iUc8NFU2XX1WQPfBRRHN6xHcrxAf9WS8BFFBn8pDrH5QPvH1tYs3lDkw6UGGbanr1RGzARqdUp1g==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.16': - resolution: {integrity: sha512-DDzmSFFKfAcrUJfuwK4URKl28fIgK8fT5Kp374B1iJJ9KwcqIZzN1a3s/ubjTGIwiE+vUDEclVQ3z9R0VwkGAQ==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.38': + resolution: {integrity: sha512-0ermTQ//WzSI0nOL3z/LUWMNiE9xeM5cLGxjewPFEexqxV/0uM8/lNp9QageQ8jfc/VO1OURsGw34HYO5PaL8w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.16': - resolution: {integrity: sha512-xkCdzCXW6SSDlFYaHjzCFrsbqxxo60YKVW4B/G2ST8HYruv0Ql4qpoQw7WoGeXL+bc/3RpKWzsxIiooUKX6e9Q==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': + resolution: {integrity: sha512-GADxzVUTCTp6EWI52831A29Tt7PukFe94nhg/SUsfkI33oTiNQtPxyLIT/3oRegizGuPSZSlrdBurkjDwxyEUQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.16': - resolution: {integrity: sha512-Yrz782pZsFVfxlsqppDneV2dl7St7lGt1uCscXnLC0vXiesj59vl3sULQ45eMKKeEEqPKz7X8OAJI7ao6zLSyg==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': + resolution: {integrity: sha512-SKO7Exl5Yem/OSNoA5uLHzyrptUQ8Hg70kHDxuwEaH0+GUg+SQe9/7PWmc4hFKBMrJGdQtii8WZ0uIz9Dofg5Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.16': - resolution: {integrity: sha512-1M8jPk7BICBjKfqNZCMtcLvzpEFHBkySPHt+RsYGZhFuAbCb352C9ilWsjpi7WwhWBOvh6tHUNmO77NTKlLxkA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': + resolution: {integrity: sha512-SOo6+WqhXPBaShLxLT0eCgH17d3Yu1lMAe4mFP0M9Bvr/kfMSOPQXuLxBcbBU9IFM9w3N6qP9xWOHO+oUJvi8Q==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.16': - resolution: {integrity: sha512-6xhZMDt4r3r3DeurJFakCqev0ct0FHU9hQPvoaHTE3EfC0yRhUp7aQmf2lsB7YVU7Zcel/KiOv/DjJQR9fntog==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': + resolution: {integrity: sha512-yvsQ3CyrodOX+lcoi+lejZGCOvJZa9xTsNB8OzpMDmHeZq3QzJfpYjXSAS6vie70fOkLVJb77UqYO193Cl8XBQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.16': - resolution: {integrity: sha512-zYnSz4Z39kEUUA1B03KbNFGgCNykZPhaDltJGY9C3bA3zU5+Ygtr+aeaRxEgXYP4PYBqE3rhPIGmDnlTzx18wA==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': + resolution: {integrity: sha512-84qzKMwUwikfYeOuJ4Kxm/3z15rt0nFGGQArHYIQQNSTiQdxGHxOkqXtzPFqrVfBJUdxBAf+jYzR1pttFJuWyg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.16': - resolution: {integrity: sha512-gFWaCVJENQWYAWkk6yJbteyMmxdZAYE9VLB4S4YqfxOYbGvVxq0K1Dn89uPEzN4beEaLToe917YzXqLdv4tPvQ==} - engines: {node: '>=14.21.3'} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': + resolution: {integrity: sha512-QrNiWlce01DYH0rL8K3yUBu+lNzY+B0DyCbIc2Atan6/S6flxOL0ow5DLQvMamOI/oKhrJ4xG+9MkMb9dDHbLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.38': + resolution: {integrity: sha512-fnLtHyjwEsG4/aNV3Uv3Qd1ZbdH+CopwJNoV0RgBqrcQB8V6/Qdikd5JKvnO23kb3QvIpP+dAMGZMv1c2PJMzw==} + engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.16': - resolution: {integrity: sha512-rbXNzlc3/aZSNaIWKAx6TGGUcgSnDmBYxyHLYthtAXz1uvg2o0YsAKYJszWHk0fTrjtKnDXLxwNjua1pf87cZA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': + resolution: {integrity: sha512-19cTfnGedem+RY+znA9J6ARBOCEFD4YSjnx0p5jiTm9tR6pHafRfFIfKlTXhun+NL0WWM/M0eb2IfPPYUa8+wg==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.16': - resolution: {integrity: sha512-9o4nk+IEvyWkE5qsLjcN+Sic869hELVZ5FsEvDruCa9sX5qZV4A5pj5bR9Sc+x4L0Aa1kQkPdChgxRqV1tgOdw==} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': + resolution: {integrity: sha512-HcICm4YzFJZV+fI0O0bFLVVlsWvRNo/AB9EfUXvNYbtAxakCnQZ15oq22deFdz6sfi9Y4/SagH2kPU723dhCFA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.16': - resolution: {integrity: sha512-PJSdUi02LT2dRS5nRNmqWTAEvq11NSBfPK5DoCTUj4DaUHJd05jBBtVyLabTutjaACN53O/pLOXds73W4obZ/g==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': + resolution: {integrity: sha512-4Qx6cgEPXLb0XsCyLoQcUgYBpfL0sjugftob+zhUH0EOk/NVCAIT+h0NJhY+jn7pFpeKxhNMqhvTNx3AesxIAQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-beta.16': - resolution: {integrity: sha512-w3f87JpF7lgIlK03I0R3XidspFgB4MsixE5o/VjBMJI+Ki4XW/Ffrykmj2AUCbVxhRD7Pi9W0Qu2XapJhB2mSA==} - '@rolldown/pluginutils@1.0.0-beta.34': resolution: {integrity: sha512-LyAREkZHP5pMom7c24meKmJCdhf2hEyvam2q0unr3or9ydwDL+DJ8chTF6Av/RFPb3rH8UFBdMzO5MxTZW97oA==} + '@rolldown/pluginutils@1.0.0-beta.38': + resolution: {integrity: sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==} + '@rollup/rollup-android-arm-eabi@4.46.2': resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} cpu: [arm] @@ -1346,6 +1402,14 @@ packages: peerDependencies: '@rsbuild/core': 1.x + '@rsbuild/plugin-type-check@1.2.4': + resolution: {integrity: sha512-0m4TRp9mTgkQ61UWnqE6cOLj/tBltXBWqLYHh8DDz+mk9qabJQ6ilTl8vQbSrg/jYH/3AksQZjlpZMEplUrE2Q==} + peerDependencies: + '@rsbuild/core': 1.x + peerDependenciesMeta: + '@rsbuild/core': + optional: true + '@rspack/binding-darwin-arm64@1.5.3': resolution: {integrity: sha512-8R1uqr5E2CzRZjsA1QLXkD4xwcsiHmLJTIzCNj9QJ4+lCw6XgtPqpHZuk3zNROLayijEKwotGXJFHJIbgv1clA==} cpu: [arm64] @@ -1514,12 +1578,17 @@ packages: '@tailwindcss/postcss@4.1.13': resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==} + '@tanstack/query-core@5.89.0': + resolution: {integrity: sha512-joFV1MuPhSLsKfTzwjmPDrp8ENfZ9N23ymFu07nLfn3JCkSHy0CFgsyhHTJOmWaumC/WiNIKM0EJyduCF/Ih/Q==} + + '@tanstack/react-query@5.89.0': + resolution: {integrity: sha512-SXbtWSTSRXyBOe80mszPxpEbaN4XPRUp/i0EfQK1uyj3KCk/c8FuPJNIRwzOVe/OU3rzxrYtiNabsAmk1l714A==} + peerDependencies: + react: ^18 || ^19 + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.9.0': - resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1614,6 +1683,10 @@ packages: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1634,6 +1707,10 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -1674,6 +1751,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1828,16 +1909,17 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fdir@6.4.4: - resolution: {integrity: sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1875,6 +1957,16 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-to-regex.js@1.0.1: + resolution: {integrity: sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + goober@2.1.14: resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} peerDependencies: @@ -1908,6 +2000,10 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + i18next-browser-languagedetector@8.2.0: resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} @@ -1930,6 +2026,10 @@ packages: resolution: {integrity: sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==} engines: {node: '>=18'} + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2087,6 +2187,10 @@ packages: material-symbols@0.35.2: resolution: {integrity: sha512-ce0Bd3ySBdiTNgmVpFQCBdwtcsYPaZkSPHeVivFRyhu/hwB7iK1aVTRaqi5xaOw8yLPOtwp7O5A+3OyUS8S+Ow==} + memfs@4.42.0: + resolution: {integrity: sha512-RG+4HMGyIVp6UWDWbFmZ38yKrSzblPnfJu0PyPt0hw52KW4PPlPp+HdV4qZBG0hLDuYVnf8wfQT4NymKXnlQjA==} + engines: {node: '>= 4.0.0'} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -2095,6 +2199,10 @@ packages: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -2128,6 +2236,10 @@ packages: node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + notistack@3.0.2: resolution: {integrity: sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==} engines: {node: '>=12.0.0', npm: '>=6.0.0'} @@ -2149,6 +2261,12 @@ packages: openapi-fetch@0.14.0: resolution: {integrity: sha512-PshIdm1NgdLvb05zp8LqRQMNSKzIlPkyMxYFxwyHR+UlKD4t2nUjkDhNxeRbhRSEd3x5EUNh2w5sJYwkhOH4fg==} + openapi-react-query@0.5.0: + resolution: {integrity: sha512-VtyqiamsbWsdSWtXmj/fAR+m9nNxztsof6h8ZIsjRj8c8UR/x9AIwHwd60IqwgymmFwo7qfSJQ1ZzMJrtqjQVg==} + peerDependencies: + '@tanstack/react-query': ^5.25.0 + openapi-fetch: ^0.14.0 + openapi-typescript-helpers@0.0.15: resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==} @@ -2182,10 +2300,6 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -2316,6 +2430,10 @@ packages: resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} engines: {node: '>=0.10.0'} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -2333,19 +2451,19 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} - rolldown-vite@6.3.21: - resolution: {integrity: sha512-mjds/3g+YPWJmT08oQic/L5sWvs/lNc4vs9vmD7uHQtGdP7qGriWtYf62Vp+6eQhd/MPeFVw71TMEEt/cH+sLQ==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + rolldown-vite@7.1.11: + resolution: {integrity: sha512-33L3z0NvLLyg2avZsEuLrKR33l8+tALVw9tYpSvW/4Zj7tYeRs5O9bodPL/NsmfUzLSjVIoG+GdADVeil0HNiQ==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@types/node': ^20.19.0 || >=22.12.0 esbuild: ^0.25.0 jiti: '>=1.21.0' - less: '*' - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -2373,8 +2491,9 @@ packages: yaml: optional: true - rolldown@1.0.0-beta.16: - resolution: {integrity: sha512-ruNh01VbnTJsW0kgYywrQ80FUY0yJvXqavPVljGg0dRiwggYB7yXlypw1ptkFiomkEOnOGiwncjiviUakgPHxg==} + rolldown@1.0.0-beta.38: + resolution: {integrity: sha512-58frPNX55Je1YsyrtPJv9rOSR3G5efUZpRqok94Efsj0EUa8dnqJV3BldShyI7A+bVPleucOtzXHwVpJRcR0kQ==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true rollup@4.46.2: @@ -2591,20 +2710,26 @@ packages: engines: {node: '>=10'} hasBin: true + thingies@2.5.0: + resolution: {integrity: sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2636,6 +2761,22 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + ts-checker-rspack-plugin@1.1.5: + resolution: {integrity: sha512-jla7C8ENhRP87i2iKo8jLMOvzyncXou12odKe0CPTkCaI9l8Eaiqxflk/ML3+1Y0j+gKjMk2jb6swHYtlpdRqg==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@rspack/core': ^1.0.0 + typescript: '>=3.8.0' + peerDependenciesMeta: + '@rspack/core': + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3144,33 +3285,17 @@ snapshots: '@csstools/css-tokenizer@3.0.3': {} - '@emnapi/core@1.4.3': - dependencies: - '@emnapi/wasi-threads': 1.0.2 - tslib: 2.8.1 - optional: true - '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.3': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.5.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.2': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 @@ -3318,6 +3443,41 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 optional: true + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.14.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.5.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + '@kurkle/color@0.3.4': {} '@module-federation/error-codes@0.18.0': {} @@ -3345,13 +3505,6 @@ snapshots: '@module-federation/runtime': 0.18.0 '@module-federation/sdk': 0.18.0 - '@napi-rs/wasm-runtime@0.2.11': - dependencies: - '@emnapi/core': 1.4.3 - '@emnapi/runtime': 1.4.3 - '@tybys/wasm-util': 0.9.0 - optional: true - '@napi-rs/wasm-runtime@1.0.5': dependencies: '@emnapi/core': 1.5.0 @@ -3359,9 +3512,9 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@oxc-project/runtime@0.73.0': {} + '@oxc-project/runtime@0.89.0': {} - '@oxc-project/types@0.73.0': {} + '@oxc-project/types@0.89.0': {} '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -3853,48 +4006,54 @@ snapshots: transitivePeerDependencies: - supports-color - '@rolldown/binding-darwin-arm64@1.0.0-beta.16': + '@rolldown/binding-android-arm64@1.0.0-beta.38': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.16': + '@rolldown/binding-darwin-arm64@1.0.0-beta.38': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.16': + '@rolldown/binding-darwin-x64@1.0.0-beta.38': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.16': + '@rolldown/binding-freebsd-x64@1.0.0-beta.38': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.16': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.38': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.16': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.38': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.16': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.38': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.16': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.38': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.16': - dependencies: - '@napi-rs/wasm-runtime': 0.2.11 + '@rolldown/binding-linux-x64-musl@1.0.0-beta.38': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.16': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.38': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.16': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.38': + dependencies: + '@napi-rs/wasm-runtime': 1.0.5 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.38': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.16': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.38': optional: true - '@rolldown/pluginutils@1.0.0-beta.16': {} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.38': + optional: true '@rolldown/pluginutils@1.0.0-beta.34': {} + '@rolldown/pluginutils@1.0.0-beta.38': {} + '@rollup/rollup-android-arm-eabi@4.46.2': optional: true @@ -3994,6 +4153,18 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.92.1 + '@rsbuild/plugin-type-check@1.2.4(@rsbuild/core@1.5.6)(@rspack/core@1.5.3(@swc/helpers@0.5.17))(typescript@5.9.2)': + dependencies: + deepmerge: 4.3.1 + json5: 2.2.3 + reduce-configs: 1.1.1 + ts-checker-rspack-plugin: 1.1.5(@rspack/core@1.5.3(@swc/helpers@0.5.17))(typescript@5.9.2) + optionalDependencies: + '@rsbuild/core': 1.5.6 + transitivePeerDependencies: + - '@rspack/core' + - typescript + '@rspack/binding-darwin-arm64@1.5.3': optional: true @@ -4133,12 +4304,14 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.13 - '@tybys/wasm-util@0.10.1': + '@tanstack/query-core@5.89.0': {} + + '@tanstack/react-query@5.89.0(react@19.1.1)': dependencies: - tslib: 2.8.1 - optional: true + '@tanstack/query-core': 5.89.0 + react: 19.1.1 - '@tybys/wasm-util@0.9.0': + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true @@ -4190,7 +4363,7 @@ snapshots: '@types/uuid@10.0.0': {} - '@vitejs/plugin-react@5.0.2(rolldown-vite@6.3.21(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2))': + '@vitejs/plugin-react@5.0.2(rolldown-vite@7.1.11(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2))': dependencies: '@babel/core': 7.28.3 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) @@ -4198,7 +4371,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.34 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: rolldown-vite@6.3.21(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2) + vite: rolldown-vite@7.1.11(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2) transitivePeerDependencies: - supports-color @@ -4259,6 +4432,11 @@ snapshots: ansis@4.1.0: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -4275,6 +4453,8 @@ snapshots: big.js@5.2.2: {} + binary-extensions@2.3.0: {} + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -4282,7 +4462,6 @@ snapshots: braces@3.0.3: dependencies: fill-range: 7.1.1 - optional: true browserslist@4.25.2: dependencies: @@ -4316,6 +4495,18 @@ snapshots: check-error@2.1.1: {} + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -4467,18 +4658,17 @@ snapshots: fast-deep-equal@3.1.3: {} - fdir@6.4.4(picomatch@4.0.2): + fdir@6.4.6(picomatch@4.0.3): optionalDependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 - fdir@6.4.6(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - optional: true framer-motion@12.23.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: @@ -4496,6 +4686,14 @@ snapshots: get-nonce@1.0.1: {} + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regex.js@1.0.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + goober@2.1.14(csstype@3.1.3): dependencies: csstype: 3.1.3 @@ -4535,6 +4733,8 @@ snapshots: transitivePeerDependencies: - supports-color + hyperdyperid@1.2.0: {} + i18next-browser-languagedetector@8.2.0: dependencies: '@babel/runtime': 7.27.3 @@ -4553,16 +4753,17 @@ snapshots: index-to-position@1.1.0: {} - is-extglob@2.1.1: - optional: true + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - optional: true - is-number@7.0.0: - optional: true + is-number@7.0.0: {} is-plain-object@5.0.0: {} @@ -4692,6 +4893,15 @@ snapshots: material-symbols@0.35.2: {} + memfs@4.42.0: + dependencies: + '@jsonjoy.com/json-pack': 1.14.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.0.1(tslib@2.8.1) + thingies: 2.5.0(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -4702,6 +4912,10 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + minipass@7.1.2: {} minizlib@3.0.2: @@ -4725,6 +4939,8 @@ snapshots: node-releases@2.0.19: {} + normalize-path@3.0.0: {} + notistack@3.0.2(csstype@3.1.3)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: clsx: 1.2.1 @@ -4746,6 +4962,12 @@ snapshots: dependencies: openapi-typescript-helpers: 0.0.15 + openapi-react-query@0.5.0(@tanstack/react-query@5.89.0(react@19.1.1))(openapi-fetch@0.14.0): + dependencies: + '@tanstack/react-query': 5.89.0(react@19.1.1) + openapi-fetch: 0.14.0 + openapi-typescript-helpers: 0.0.15 + openapi-typescript-helpers@0.0.15: {} openapi-typescript@7.9.1(typescript@5.9.2): @@ -4776,10 +4998,7 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: - optional: true - - picomatch@4.0.2: {} + picomatch@2.3.1: {} picomatch@4.0.3: {} @@ -4889,6 +5108,10 @@ snapshots: react@19.1.1: {} + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + readdirp@4.1.2: optional: true @@ -4900,15 +5123,15 @@ snapshots: require-from-string@2.0.2: {} - rolldown-vite@6.3.21(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2): + rolldown-vite@7.1.11(@types/node@24.4.0)(esbuild@0.25.8)(jiti@2.5.1)(sass-embedded@1.92.1)(sass@1.92.1)(terser@5.31.0)(yaml@2.4.2): dependencies: - '@oxc-project/runtime': 0.73.0 - fdir: 6.4.4(picomatch@4.0.2) + '@oxc-project/runtime': 0.89.0 + fdir: 6.5.0(picomatch@4.0.3) lightningcss: 1.30.1 - picomatch: 4.0.2 + picomatch: 4.0.3 postcss: 8.5.6 - rolldown: 1.0.0-beta.16 - tinyglobby: 0.2.13 + rolldown: 1.0.0-beta.38 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.4.0 esbuild: 0.25.8 @@ -4919,25 +5142,26 @@ snapshots: terser: 5.31.0 yaml: 2.4.2 - rolldown@1.0.0-beta.16: + rolldown@1.0.0-beta.38: dependencies: - '@oxc-project/runtime': 0.73.0 - '@oxc-project/types': 0.73.0 - '@rolldown/pluginutils': 1.0.0-beta.16 + '@oxc-project/types': 0.89.0 + '@rolldown/pluginutils': 1.0.0-beta.38 ansis: 4.1.0 optionalDependencies: - '@rolldown/binding-darwin-arm64': 1.0.0-beta.16 - '@rolldown/binding-darwin-x64': 1.0.0-beta.16 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.16 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.16 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.16 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.16 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.16 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.16 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.16 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.16 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.16 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.16 + '@rolldown/binding-android-arm64': 1.0.0-beta.38 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.38 + '@rolldown/binding-darwin-x64': 1.0.0-beta.38 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.38 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.38 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.38 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.38 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.38 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.38 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.38 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.38 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.38 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.38 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.38 rollup@4.46.2: dependencies: @@ -5165,20 +5389,24 @@ snapshots: source-map-support: 0.5.21 optional: true + thingies@2.5.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tinybench@2.9.0: {} tinyexec@0.3.2: {} - tinyglobby@0.2.13: - dependencies: - fdir: 6.4.4(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tinypool@1.1.1: {} tinyrainbow@2.0.0: {} @@ -5194,7 +5422,6 @@ snapshots: to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - optional: true tough-cookie@5.1.2: dependencies: @@ -5204,6 +5431,23 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + + ts-checker-rspack-plugin@1.1.5(@rspack/core@1.5.3(@swc/helpers@0.5.17))(typescript@5.9.2): + dependencies: + '@babel/code-frame': 7.27.1 + '@rspack/lite-tapable': 1.0.1 + chokidar: 3.6.0 + is-glob: 4.0.3 + memfs: 4.42.0 + minimatch: 9.0.5 + picocolors: 1.1.1 + typescript: 5.9.2 + optionalDependencies: + '@rspack/core': 1.5.3(@swc/helpers@0.5.17) + tslib@2.8.1: {} type-fest@4.41.0: {} diff --git a/ui/rsbuild.config.ts b/ui/rsbuild.config.ts index 8a2b6bf88..c4cc3891d 100644 --- a/ui/rsbuild.config.ts +++ b/ui/rsbuild.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from '@rsbuild/core'; import { pluginReact } from '@rsbuild/plugin-react'; import { pluginBabel } from '@rsbuild/plugin-babel'; import { pluginSass } from '@rsbuild/plugin-sass'; +import { pluginTypeCheck } from "@rsbuild/plugin-type-check"; const ReactCompilerConfig = { // ReactCompilerConfig hier einfügen, falls benötigt @@ -19,12 +20,14 @@ export default defineConfig({ }, }), pluginSass(), + pluginTypeCheck() ], source: { entry: { main: './src/main.tsx' } }, + html: { template: './index.html', }, diff --git a/ui/schema.d.ts b/ui/schema.d.ts index 606ee5a8e..21a0026b4 100644 --- a/ui/schema.d.ts +++ b/ui/schema.d.ts @@ -1308,6 +1308,7 @@ export interface components { explicitConsent: boolean; /** Format: int32 */ id: number; + readOnly: boolean; role: string; username: string; }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 3831ff7d1..26b1991ed 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -4,7 +4,6 @@ import {useTranslation} from 'react-i18next' import {enqueueSnackbar} from 'notistack' import useCommon from './store/CommonSlice' import useOpmlImport from './store/opmlImportSlice' -import {decodeHTMLEntities} from './utils/Utilities' import { EpisodeSearchViewLazyLoad, HomepageViewLazyLoad, @@ -33,9 +32,10 @@ import {UserManagementPage} from "./pages/UserManagement"; import {GPodderIntegration} from "./pages/GPodderIntegration"; import {TagsPage} from "./pages/TagsPage"; import {components} from "../schema"; -import {client} from "./utils/http"; +import {$api, client} from "./utils/http"; import io, {Socket} from "socket.io-client" import {ClientToServerEvents, ServerToClientEvents} from "./models/socketioEvents"; +import {decodeHTMLEntities} from "./utils/decodingUtilities"; export const router = createBrowserRouter(createRoutesFromElements( <> @@ -197,19 +197,6 @@ const App: FC = ({ children }) => { }) }, [socket]) - useEffect(() => { - if (config?.basicAuth||config?.oidcConfigured||config?.reverseProxy){ - client.GET("/api/v1/users/{username}", { - params: { - path: { - username: "me" - } - } - }).then((c)=>useCommon.getState().setLoggedInUser(c.data!)) - .catch(() => enqueueSnackbar(t('not-admin'), { variant: 'error' })) - } - }, []); - const getNotifications = () => { client.GET("/api/v1/notifications/unread") .then((response) => { diff --git a/ui/src/components/CustomInput.tsx b/ui/src/components/CustomInput.tsx index 035245da4..d6fe7c91f 100644 --- a/ui/src/components/CustomInput.tsx +++ b/ui/src/components/CustomInput.tsx @@ -1,8 +1,9 @@ import {ChangeEventHandler, FC, InputHTMLAttributes} from 'react' -export const CustomInput: FC> = ({ autoComplete, onBlur, className = '', id, name, onChange, disabled, placeholder, required, type = 'text', value }) => { +export const CustomInput: FC> = ({ autoComplete, onBlur, className = '', id, name, onChange, disabled, placeholder, required, type = 'text', value, ...props }) => { return ( - + ) } diff --git a/ui/src/components/EpisodeSearch.tsx b/ui/src/components/EpisodeSearch.tsx index 2cb918a5a..1bf1499d0 100644 --- a/ui/src/components/EpisodeSearch.tsx +++ b/ui/src/components/EpisodeSearch.tsx @@ -6,7 +6,7 @@ import { useDebounce } from '../utils/useDebounce' import { CustomInput } from './CustomInput' import { Spinner } from './Spinner' import { EmptyResultIcon } from '../icons/EmptyResultIcon' -import {client} from "../utils/http"; +import {$api, client} from "../utils/http"; import {components} from "../../schema"; type EpisodeSearchProps = { @@ -23,6 +23,15 @@ export const EpisodeSearch: FC = ({ classNameResults = '', o const [searchName, setSearchName] = useState('') const [searchResults, setSearchResults] = useState([]) const { t } = useTranslation() + const {data, isLoading} = $api.useQuery('get', '/api/v1/users/{username}', { + params: { + path: { + username: 'me' + } + } + }) + + const performSearch = () => { if (searchName.trim().length > 0) { @@ -43,6 +52,12 @@ export const EpisodeSearch: FC = ({ classNameResults = '', o useDebounce(performSearch, 500, [searchName]) + if (isLoading || !data) { + return
+ +
+ } + return ( <> {/* Search field */} @@ -76,7 +91,7 @@ export const EpisodeSearch: FC = ({ classNameResults = '', o {episode.name} + " src={prependAPIKeyOnAuthEnabled(episode.image_url, data)} /> {/* Information */}
diff --git a/ui/src/components/PodcastCard.tsx b/ui/src/components/PodcastCard.tsx index 5349792c2..b27e060da 100644 --- a/ui/src/components/PodcastCard.tsx +++ b/ui/src/components/PodcastCard.tsx @@ -10,7 +10,8 @@ import {CustomCheckbox} from "./CustomCheckbox"; import {LuTags} from "react-icons/lu"; import {useTranslation} from "react-i18next"; import {components} from "../../schema"; -import {client} from "../utils/http"; +import {$api, client} from "../utils/http"; +import {Loading} from "./Loading"; type PodcastCardProps = { podcast: components["schemas"]["PodcastDto"] @@ -33,6 +34,19 @@ export const PodcastCard: FC = ({podcast}) => { } const {t} = useTranslation() const [newTag, setNewTag] = useState('') + const {data, error, isLoading} = $api.useQuery('get', '/api/v1/users/{username}', { + params: { + path: { + username: 'me' + } + }, + }) + + + if (isLoading ||!data) { + return + } + return ( { @@ -43,7 +57,7 @@ export const PodcastCard: FC = ({podcast}) => {
+ src={prependAPIKeyOnAuthEnabled(podcast.image_url, data)} alt=""/> { @@ -15,9 +16,18 @@ const AccountTrigger = ()=>{ export const UserMenu: FC = () => { const config = useCommon(state => state.configModel) const configModel = useCommon(state => state.configModel) - const loggedInUser = useCommon(state => state.loggedInUser) + const {data, error, isLoading} = $api.useQuery('get', '/api/v1/users/{username}', { + params: { + path: { + username: 'me' + } + }, + }) const menuItems: Array = useMemo(()=>{ + if (isLoading || !data) { + return [] + } const menuItems: Array = [ { iconName: 'info', @@ -32,7 +42,7 @@ export const UserMenu: FC = () => { path: 'profile' }) - if (loggedInUser?.role === 'admin' || !(config?.oidcConfigured && config.basicAuth)) { + if (data.role === 'admin' || !(config?.oidcConfigured && config.basicAuth)) { menuItems.push({ iconName: 'settings', translationKey: 'settings', @@ -70,7 +80,7 @@ export const UserMenu: FC = () => { }) } return menuItems - }, [configModel,config, loggedInUser]) + }, [configModel,config, data]) return ( diff --git a/ui/src/language/json/de.json b/ui/src/language/json/de.json index 514d7fdf7..67688cd49 100644 --- a/ui/src/language/json/de.json +++ b/ui/src/language/json/de.json @@ -193,5 +193,9 @@ "add": "Hinzufügen", "tag_one": "Tag", "tag_other": "Tags", - "unplayed": "Ungehört" + "unplayed": "Ungehört", + + + + "UPDATE_OF_ADMIN_NOT_ALLOWED": "Das Ändern des Administratoraccounts {{username}} ist nicht erlaubt" } diff --git a/ui/src/pages/PodcastDetailPage.tsx b/ui/src/pages/PodcastDetailPage.tsx index 14f12c973..34c7347bc 100644 --- a/ui/src/pages/PodcastDetailPage.tsx +++ b/ui/src/pages/PodcastDetailPage.tsx @@ -14,8 +14,9 @@ import 'material-symbols/outlined.css' import {PodcastEpisodeAlreadyPlayed} from "../components/PodcastEpisodeAlreadyPlayed"; import {ErrorIcon} from "../icons/ErrorIcon"; import {PodcastSettingsModal} from "../components/PodcastSettingsModal"; -import { client } from '../utils/http' +import {$api, client} from '../utils/http' import {EditableHeading} from "../components/EditableHeading"; +import {Loading} from "../components/Loading"; export const PodcastDetailPage = () => { const configModel = useCommon(state => state.configModel) @@ -30,7 +31,13 @@ export const PodcastDetailPage = () => { const setSelectedEpisodes = useCommon(state => state.setSelectedEpisodes) const [openSettingsMenu, setOpenSettingsMenu] = useState(false) const [onlyUnplayed, setOnlyUnplayed] = useState(false) - const loggedInUser = useCommon(state => state.loggedInUser) + const {data, error, isLoading} = $api.useQuery('get', '/api/v1/users/{username}', { + params: { + path: { + username: 'me' + } + }, + }) useEffect(() => { if (params && !isNaN(parseFloat(params.id as string))) { @@ -111,6 +118,10 @@ export const PodcastDetailPage = () => {
} + if (isLoading ||!data) { + return + } + return (
@@ -131,7 +142,7 @@ export const PodcastDetailPage = () => { xs:col-start-1 xs:col-end-2 row-start-3 row-end-4 lg:col-start-1 lg:col-end-2 lg:row-start-2 lg:row-end-4 w-full xs:w-24 md:w-32 lg:w-40 rounded-xl - " src={prependAPIKeyOnAuthEnabled(currentPodcast.image_url)} alt=""/> + " src={prependAPIKeyOnAuthEnabled(currentPodcast.image_url, data)} alt=""/> {/* Title and refresh icon */}
- + { diff --git a/ui/src/pages/Podcasts.tsx b/ui/src/pages/Podcasts.tsx index cc2198e27..59fdc2b9b 100644 --- a/ui/src/pages/Podcasts.tsx +++ b/ui/src/pages/Podcasts.tsx @@ -61,6 +61,7 @@ export const Podcasts: FC = ({ onlyFavorites }) => { const setTags = useCommon(state=>state.setPodcastTags) + const memorizedSelection = useMemo(() => { return JSON.stringify({sorting: filters?.filter?.toUpperCase(), ascending: filters?.ascending}) }, [filters]) diff --git a/ui/src/pages/SystemInfoPage.tsx b/ui/src/pages/SystemInfoPage.tsx index b10d29b84..1359b0d07 100644 --- a/ui/src/pages/SystemInfoPage.tsx +++ b/ui/src/pages/SystemInfoPage.tsx @@ -7,7 +7,7 @@ import { Heading3 } from '../components/Heading3' import { Loading } from '../components/Loading' import 'material-symbols/outlined.css' import useCommon from "../store/CommonSlice"; -import {client} from "../utils/http"; +import {$api, client} from "../utils/http"; import {components} from "../../schema"; type VersionInfoModel = { @@ -23,6 +23,13 @@ export const SystemInfoPage: FC = () => { const configModel = useCommon(state => state.configModel) const [systemInfo, setSystemInfo] = useState() const [versionInfo, setVersionInfo] = useState() + const {data, error, isLoading} = $api.useQuery('get', '/api/v1/users/{username}', { + params: { + path: { + username: 'me' + } + }, + }) const { t } = useTranslation() const gigaByte = Math.pow(10,9) @@ -67,6 +74,10 @@ export const SystemInfoPage: FC = () => { } } + if (isLoading ||!data) { + return + } + return ( <> @@ -135,7 +146,7 @@ export const SystemInfoPage: FC = () => {
{t('rss-feed')}
-
{prependAPIKeyOnAuthEnabled(configModel!.rssFeed)}
+
{prependAPIKeyOnAuthEnabled(configModel!.rssFeed, data)}
{versionInfo && ( <> diff --git a/ui/src/pages/UserManagement.tsx b/ui/src/pages/UserManagement.tsx index 127c5c7ae..6b1239b0e 100644 --- a/ui/src/pages/UserManagement.tsx +++ b/ui/src/pages/UserManagement.tsx @@ -7,8 +7,10 @@ import {Controller, useForm} from "react-hook-form"; import {CustomButtonPrimary} from "../components/CustomButtonPrimary"; import {v4} from "uuid"; import {enqueueSnackbar} from "notistack"; -import {client} from "../utils/http"; +import {$api, client} from "../utils/http"; import {components} from "../../schema"; +import {useQueryClient} from "@tanstack/react-query"; +import {APIError} from "../utils/ErrorDefinition"; type UserManagementPageProps = { @@ -17,39 +19,61 @@ type UserManagementPageProps = { export const UserManagementPage: FC = () => { const {t} = useTranslation() - const loggedInUser = useCommon(state => state.loggedInUser) - + const queryClient = useQueryClient() + const {data, error, isLoading} = $api.useQuery('get', '/api/v1/users/{username}', { + params: { + path: { + username: 'me' + } + }, + }) + const updateProfile = $api.useMutation('put', '/api/v1/users/{username}') const {control, handleSubmit, setValue} = useForm({ defaultValues: { username: '', apiKey: '' }}) + useEffect(() => { - if (loggedInUser) { - setValue('username', loggedInUser.username) + if (data && data.username) { + setValue('username', data.username) } - }, [loggedInUser]) + }, [data]) const update_settings = (data: components["schemas"]["UserCoreUpdateModel"]) => { if (data.password === '') { delete data.password } - client.PUT("/api/v1/users/{username}", { + + updateProfile.mutateAsync({ + body: data, params: { path: { - username: loggedInUser!.username + username: data!.username } }, - body: data - }).then((resp)=>{ - if (!resp.error) { - useCommon.getState().setLoggedInUser(resp.data!) - enqueueSnackbar(t('user-settings-updated'), {variant: 'success'}) + }).then(()=>{ + queryClient.setQueryData(['get', '/api/v1/users/{username}'], (oldData: any) => { + return { + ...oldData, + ...data + } + }) + }) + .catch(e=>{ + if (e instanceof APIError) { + enqueueSnackbar(t(e.details?.errorCode, e.details.arguments), {variant: 'error'}) + } else { + enqueueSnackbar(e.message, {variant: 'error'}) } }) } + if (isLoading || !data) { + return
{t('loading')}...
+ } + return (
{t('profile')} @@ -60,7 +84,7 @@ export const UserManagementPage: FC = () => { htmlFor="username">{t('username')} ( - )}/> @@ -68,7 +92,7 @@ export const UserManagementPage: FC = () => { htmlFor="password">{t('password')} ( - )}/> @@ -78,18 +102,16 @@ export const UserManagementPage: FC = () => { render={({field: {name, onChange, value}}) => (
-
)}/>
- { - - }}>{t('save')} + {t('save')}
diff --git a/ui/src/routing/Root.tsx b/ui/src/routing/Root.tsx index 8451cbc0a..4a5bbab69 100644 --- a/ui/src/routing/Root.tsx +++ b/ui/src/routing/Root.tsx @@ -9,6 +9,9 @@ import { Loading } from '../components/Loading' import { MainContentPanel } from '../components/MainContentPanel' import { Sidebar } from '../components/Sidebar' import {configWSUrl} from "../utils/navigationUtils"; +import {QueryClientProvider, QueryClient} from "@tanstack/react-query"; + +const queryClient = new QueryClient() export const Root = () => { const configModel = useCommon(state => state.configModel) @@ -57,6 +60,7 @@ export const Root = () => { return ( +
@@ -70,5 +74,6 @@ export const Root = () => {
+
) } diff --git a/ui/src/store/CommonSlice.ts b/ui/src/store/CommonSlice.ts index 3a119d9e7..7a6f7327a 100644 --- a/ui/src/store/CommonSlice.ts +++ b/ui/src/store/CommonSlice.ts @@ -46,8 +46,6 @@ export type PodcastEpisode = { // Define a type for the slice state interface CommonProps { selectedEpisodes: components["schemas"]["PodcastEpisodeWithHistory"][], - loggedInUser: components["schemas"]["UserWithAPiKey"]|undefined, - setLoggedInUser: (loggedInUser: components["schemas"]["UserWithAPiKey"]) => void, sidebarCollapsed: boolean, podcasts: components["schemas"]["PodcastDto"][], searchedPodcasts: AgnosticPodcastDataModel[]|undefined, @@ -218,8 +216,6 @@ const useCommon = create((set, get) => ({ setInfoText: (infoText: string) => set({infoText}), setPodcastAlreadyPlayed: (podcastAlreadyPlayed: boolean) => set({podcastAlreadyPlayed}), setPodcastEpisodeAlreadyPlayed: (podcastEpisodeAlreadyPlayed) => set({podcastEpisodeAlreadyPlayed}), - setLoggedInUser: (loggedInUser: components["schemas"]["UserWithoutPassword"]) => set({loggedInUser}), - loggedInUser: undefined, tags: [], setPodcastTags: (t)=>set({tags: t}), headers:{ diff --git a/ui/src/utils/ErrorDefinition.ts b/ui/src/utils/ErrorDefinition.ts new file mode 100644 index 000000000..403f0d0a1 --- /dev/null +++ b/ui/src/utils/ErrorDefinition.ts @@ -0,0 +1,11 @@ +export class APIError extends Error { + details: { + errorCode: string; + arguments?: Record; + } + + constructor(details: { errorCode: string; arguments?: Record; } = {errorCode: "UNKNOWN_ERROR"}) { + super(); + this.details = details; + } +} \ No newline at end of file diff --git a/ui/src/utils/Utilities.tsx b/ui/src/utils/Utilities.tsx index 0c811b16b..f080c755b 100644 --- a/ui/src/utils/Utilities.tsx +++ b/ui/src/utils/Utilities.tsx @@ -6,11 +6,9 @@ import fr from 'javascript-time-ago/locale/fr' import pl from 'javascript-time-ago/locale/pl' import es from 'javascript-time-ago/locale/es' import i18n from "i18next"; -import useCommon, {PodcastEpisode} from "../store/CommonSlice"; -import {PodcastWatchedModel} from "../models/PodcastWatchedModel"; +import useCommon from "../store/CommonSlice"; import {Filter} from "../models/Filter"; import {OrderCriteria} from "../models/Order"; -import {Episode} from "../models/Episode"; import {components} from "../../schema"; const defaultOptions: IOptions = { @@ -74,15 +72,15 @@ export const preparePodcastEpisode = (episode: components["schemas"]["PodcastEpi } -export const prependAPIKeyOnAuthEnabled = (url: string)=>{ - if (useCommon.getState().loggedInUser?.apiKey && (useCommon.getState().configModel?.oidcConfig||useCommon.getState().configModel?.basicAuth)) { +export const prependAPIKeyOnAuthEnabled = (url: string, loggedInUser: components['schemas']['UserWithAPiKey'])=>{ + if (loggedInUser.apiKey && (useCommon.getState().configModel?.oidcConfig||useCommon.getState().configModel?.basicAuth)) { if (url.includes('?')) { url += '&' } else { url += '?' } - url += 'apiKey=' + useCommon.getState().loggedInUser?.apiKey + url += 'apiKey=' + loggedInUser?.apiKey } return url } @@ -136,9 +134,4 @@ export const TITLE_DESCENDING:OrderCriteriaSortingType = { ascending: false } -export const decodeHTMLEntities = (html: string): string => { - const textArea = document.createElement('textarea'); - textArea.innerHTML = html; - textArea.remove() - return textArea.value; -} + diff --git a/ui/src/utils/decodingUtilities.ts b/ui/src/utils/decodingUtilities.ts new file mode 100644 index 000000000..161a718a4 --- /dev/null +++ b/ui/src/utils/decodingUtilities.ts @@ -0,0 +1,6 @@ +export const decodeHTMLEntities = (html: string): string => { + const textArea = document.createElement('textarea'); + textArea.innerHTML = html; + textArea.remove() + return textArea.value; +} \ No newline at end of file diff --git a/ui/src/utils/http.ts b/ui/src/utils/http.ts index ee770c2b2..42f43197e 100644 --- a/ui/src/utils/http.ts +++ b/ui/src/utils/http.ts @@ -1,4 +1,7 @@ import createClient, {Middleware} from "openapi-fetch"; +import createTanstackQueryClient from "openapi-react-query"; +import type {paths} from "../../schema"; +import {APIError} from "./ErrorDefinition"; export let apiURL: string export let uiURL: string @@ -11,9 +14,6 @@ if (window.location.pathname.startsWith("/ui")) { } uiURL = window.location.protocol + "//" + window.location.hostname + ":" + window.location.port + "/ui" -import type { paths } from "../../schema"; -import useCommon from "../store/CommonSlice"; - export const client = createClient({ baseUrl: apiURL }); @@ -31,18 +31,32 @@ export const addHeader = (key: string, value: string) => { localStorage.getItem("auth") !== null && addHeader("Authorization", "Basic " + localStorage.getItem("auth")) sessionStorage.getItem("auth") !== null && addHeader("Authorization", "Basic " + sessionStorage.getItem("auth")) +function isJsonString(str: string) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; +} + const authMiddleware: Middleware = { async onRequest({ request}) { - Object.entries(HEADER_TO_USE).forEach(([key, value]) => { request.headers.set(key, value) }) return request; }, async onResponse({ response }) { - if (!response.ok) { - throw new Error("Request failed: " + response.body === null? response.statusText: await response.text() ); + if (response.body != null) { + const textData = await response.text() + if (isJsonString(textData)) { + throw new APIError(JSON.parse(textData)) + } else { + throw new Error("Request failed: " + response.body === null? response.statusText: await response.text()); + } + } } return response; @@ -51,6 +65,9 @@ const authMiddleware: Middleware = { client.use(authMiddleware) +export const $api = createTanstackQueryClient(client); + + client.GET("/api/v1/sys/config", { headers: { diff --git a/ui/test/HtmlCodeReplacement.test.spec.ts b/ui/test/HtmlCodeReplacement.test.spec.ts index d76fcbf0b..a0510c6d3 100644 --- a/ui/test/HtmlCodeReplacement.test.spec.ts +++ b/ui/test/HtmlCodeReplacement.test.spec.ts @@ -1,5 +1,5 @@ import {describe, it, expect} from "vitest"; -import {decodeHTMLEntities} from "../src/utils/Utilities"; +import {decodeHTMLEntities} from "../src/utils/decodingUtilities"; describe("Html code replacement", () => { it("should replace the html code", () => { diff --git a/ui/vitest.config.ts b/ui/vitest.config.ts new file mode 100644 index 000000000..fad459ba7 --- /dev/null +++ b/ui/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + }, +}); \ No newline at end of file