diff --git a/apps/recorder/.env.dev b/apps/recorder/.env.development similarity index 96% rename from apps/recorder/.env.dev rename to apps/recorder/.env.development index be63e0d..84b53c6 100644 --- a/apps/recorder/.env.dev +++ b/apps/recorder/.env.development @@ -4,6 +4,7 @@ STORAGE_DATA_DIR = "./data" AUTH_TYPE = "basic" # or oidc BASIC_USER = "konobangu" BASIC_PASSWORD = "konobangu" +LOG_LEVEL = "debug" # OIDC_ISSUER="https://auth.logto.io/oidc" # OIDC_AUDIENCE = "https://konobangu.com/api" # OIDC_CLIENT_ID = "client_id" diff --git a/apps/recorder/.env b/apps/recorder/.env.production.example similarity index 80% rename from apps/recorder/.env rename to apps/recorder/.env.production.example index 99d670f..d70d99d 100644 --- a/apps/recorder/.env +++ b/apps/recorder/.env.production.example @@ -4,6 +4,7 @@ STORAGE_DATA_DIR = "./data" AUTH_TYPE = "basic" # or oidc BASIC_USER = "konobangu" BASIC_PASSWORD = "konobangu" +LOG_LEVEL = "info" # OIDC_ISSUER="https://auth.logto.io/oidc" # OIDC_AUDIENCE = "https://konobangu.com/api" # OIDC_CLIENT_ID = "client_id" @@ -11,7 +12,3 @@ BASIC_PASSWORD = "konobangu" # OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" # OIDC_EXTRA_CLAIM_KEY = "" # OIDC_EXTRA_CLAIM_VALUE = "" -# MIKAN_PROXY = "" -# MIKAN_PROXY_AUTH_HEADER = "" -# MIKAN_NO_PROXY = "" -# MIKAN_PROXY_ACCEPT_INVALID_CERTS = "true" diff --git a/apps/recorder/.gitignore b/apps/recorder/.gitignore index 3d2eadd..2317e94 100644 --- a/apps/recorder/.gitignore +++ b/apps/recorder/.gitignore @@ -28,4 +28,6 @@ dist/ temp/* !temp/.gitkeep tests/resources/mikan/classic_episodes/*/* -!tests/resources/mikan/classic_episodes/parquet/tiny.parquet \ No newline at end of file +!tests/resources/mikan/classic_episodes/parquet/tiny.parquet +webui/ +data/ \ No newline at end of file diff --git a/apps/recorder/Cargo.toml b/apps/recorder/Cargo.toml index e4ba11a..1486d5b 100644 --- a/apps/recorder/Cargo.toml +++ b/apps/recorder/Cargo.toml @@ -126,7 +126,7 @@ seaography = { version = "1.1", features = [ "with-postgres-array", "with-json-as-scalar", ] } -tower = "0.5.2" +tower = { version = "0.5.2", features = ["util"] } tower-http = { version = "0.6", features = [ "trace", "catch-panic", diff --git a/apps/recorder/recorder.config.toml b/apps/recorder/recorder.config.toml index 6a72972..54c695d 100644 --- a/apps/recorder/recorder.config.toml +++ b/apps/recorder/recorder.config.toml @@ -4,8 +4,8 @@ enable = true # Enable pretty backtrace (sets RUST_BACKTRACE=1) pretty_backtrace = true +level = '{{ get_env(name="LOG_LEVEL", default="info") }}' # Log level, options: trace, debug, info, warn or error. -level = "debug" # Define the logging format. options: compact, pretty or Json format = "compact" # By default the logger has filtering only logs that came from your code or logs that came from `loco` framework. to see all third party libraries diff --git a/apps/recorder/src/app/builder.rs b/apps/recorder/src/app/builder.rs index d5caab8..9644ba9 100644 --- a/apps/recorder/src/app/builder.rs +++ b/apps/recorder/src/app/builder.rs @@ -72,6 +72,11 @@ impl AppBuilder { } pub async fn build(self) -> RecorderResult { + if self.working_dir != "." { + std::env::set_current_dir(&self.working_dir)?; + println!("set current dir to working dir: {}", self.working_dir); + } + self.load_env().await?; let config = self.load_config().await?; @@ -86,22 +91,12 @@ impl AppBuilder { } pub async fn load_env(&self) -> RecorderResult<()> { - AppConfig::load_dotenv( - &self.environment, - &self.working_dir, - self.dotenv_file.as_deref(), - ) - .await?; + AppConfig::load_dotenv(&self.environment, self.dotenv_file.as_deref()).await?; Ok(()) } pub async fn load_config(&self) -> RecorderResult { - let config = AppConfig::load_config( - &self.environment, - &self.working_dir, - self.config_file.as_deref(), - ) - .await?; + let config = AppConfig::load_config(&self.environment, self.config_file.as_deref()).await?; Ok(config) } @@ -136,7 +131,7 @@ impl AppBuilder { } pub fn working_dir_from_manifest_dir(self) -> Self { - let manifest_dir = if cfg!(debug_assertions) || cfg!(test) { + let manifest_dir = if cfg!(debug_assertions) || cfg!(test) || cfg!(feature = "playground") { env!("CARGO_MANIFEST_DIR") } else { "./apps/recorder" diff --git a/apps/recorder/src/app/config/mod.rs b/apps/recorder/src/app/config/mod.rs index 84400aa..88a4b39 100644 --- a/apps/recorder/src/app/config/mod.rs +++ b/apps/recorder/src/app/config/mod.rs @@ -55,8 +55,8 @@ impl AppConfig { format!(".{}.local", environment.full_name()), format!(".{}.local", environment.short_name()), String::from(".local"), - environment.full_name().to_string(), - environment.short_name().to_string(), + format!(".{}", environment.full_name()), + format!(".{}", environment.short_name()), String::from(""), ] } @@ -88,13 +88,12 @@ impl AppConfig { pub async fn load_dotenv( environment: &Environment, - working_dir: &str, dotenv_file: Option<&str>, ) -> RecorderResult<()> { let try_dotenv_file_or_dirs = if dotenv_file.is_some() { vec![dotenv_file] } else { - vec![Some(working_dir)] + vec![Some(".")] }; let priority_suffix = &AppConfig::priority_suffix(environment); @@ -111,11 +110,16 @@ impl AppConfig { for f in try_filenames.iter() { let p = try_dotenv_file_or_dir_path.join(f); if p.exists() && p.is_file() { + println!("Loading dotenv file: {}", p.display()); dotenvy::from_path(p)?; break; } } } else if try_dotenv_file_or_dir_path.is_file() { + println!( + "Loading dotenv file: {}", + try_dotenv_file_or_dir_path.display() + ); dotenvy::from_path(try_dotenv_file_or_dir_path)?; break; } @@ -127,13 +131,12 @@ impl AppConfig { pub async fn load_config( environment: &Environment, - working_dir: &str, config_file: Option<&str>, ) -> RecorderResult { let try_config_file_or_dirs = if config_file.is_some() { vec![config_file] } else { - vec![Some(working_dir)] + vec![Some(".")] }; let allowed_extensions = &AppConfig::allowed_extension(); @@ -159,6 +162,7 @@ impl AppConfig { let p = try_config_file_or_dir_path.join(f); if p.exists() && p.is_file() { fig = AppConfig::merge_provider_from_file(fig, &p, ext)?; + println!("Loaded config file: {}", p.display()); break; } } @@ -169,6 +173,10 @@ impl AppConfig { { fig = AppConfig::merge_provider_from_file(fig, try_config_file_or_dir_path, ext)?; + println!( + "Loaded config file: {}", + try_config_file_or_dir_path.display() + ); break; } } diff --git a/apps/recorder/src/app/core.rs b/apps/recorder/src/app/core.rs index 85d11cc..046cc60 100644 --- a/apps/recorder/src/app/core.rs +++ b/apps/recorder/src/app/core.rs @@ -1,11 +1,13 @@ use std::{net::SocketAddr, sync::Arc}; -use axum::Router; +use axum::{Router, middleware::from_fn_with_state}; use tokio::{net::TcpSocket, signal}; +use tower_http::services::{ServeDir, ServeFile}; use tracing::instrument; use super::{builder::AppBuilder, context::AppContextTrait}; use crate::{ + auth::webui_auth_middleware, errors::{RecorderError, RecorderResult}, web::{ controller::{self, core::ControllerTrait}, @@ -58,13 +60,19 @@ impl App { controller::oidc::create(context.clone()), controller::metadata::create(context.clone()), controller::r#static::create(context.clone()), - controller::feeds::create(context.clone()), + controller::feeds::create(context.clone()) )?; for c in [graphql_c, oidc_c, metadata_c, static_c, feeds_c] { router = c.apply_to(router); } + router = router + .fallback_service( + ServeDir::new("webui").not_found_service(ServeFile::new("webui/index.html")), + ) + .layer(from_fn_with_state(context.clone(), webui_auth_middleware)); + let middlewares = default_middleware_stack(context.clone()); for mid in middlewares { if mid.is_enabled() { diff --git a/apps/recorder/src/auth/middleware.rs b/apps/recorder/src/auth/middleware.rs index 540f49e..925642f 100644 --- a/apps/recorder/src/auth/middleware.rs +++ b/apps/recorder/src/auth/middleware.rs @@ -7,7 +7,10 @@ use axum::{ response::{IntoResponse, Response}, }; -use crate::{app::AppContextTrait, auth::AuthServiceTrait}; +use crate::{ + app::AppContextTrait, + auth::{AuthService, AuthServiceTrait}, +}; pub async fn auth_middleware( State(ctx): State>, @@ -38,3 +41,37 @@ pub async fn auth_middleware( response } + +pub async fn webui_auth_middleware( + State(ctx): State>, + request: Request, + next: Next, +) -> Response { + if (!request.uri().path().starts_with("/api")) + && let AuthService::Basic(auth_service) = ctx.auth() + { + let (mut parts, body) = request.into_parts(); + + let mut response = match auth_service + .extract_user_info(ctx.as_ref() as &dyn AppContextTrait, &mut parts) + .await + { + Ok(auth_user_info) => { + let mut request = Request::from_parts(parts, body); + request.extensions_mut().insert(auth_user_info); + next.run(request).await + } + Err(auth_error) => auth_error.into_response(), + }; + + if let Some(header_value) = auth_service.www_authenticate_header_value() { + response + .headers_mut() + .insert(header::WWW_AUTHENTICATE, header_value); + }; + + response + } else { + next.run(request).await + } +} diff --git a/apps/recorder/src/auth/mod.rs b/apps/recorder/src/auth/mod.rs index b431b37..be7a04b 100644 --- a/apps/recorder/src/auth/mod.rs +++ b/apps/recorder/src/auth/mod.rs @@ -7,5 +7,5 @@ pub mod service; pub use config::{AuthConfig, BasicAuthConfig, OidcAuthConfig}; pub use errors::AuthError; -pub use middleware::auth_middleware; +pub use middleware::{auth_middleware, webui_auth_middleware}; pub use service::{AuthService, AuthServiceTrait, AuthUserInfo}; diff --git a/apps/recorder/src/extract/http.rs b/apps/recorder/src/extract/http.rs index 9810369..6d12cbe 100644 --- a/apps/recorder/src/extract/http.rs +++ b/apps/recorder/src/extract/http.rs @@ -167,6 +167,7 @@ impl ForwardedRelatedInfo { .as_ref() .and_then(|s| s.host.as_deref()) .or(self.x_forwarded_host.as_deref()) + .or(self.host.as_deref()) .or(self.uri.host()) } diff --git a/apps/recorder/src/extract/mikan/web.rs b/apps/recorder/src/extract/mikan/web.rs index fe952ee..38c9cad 100644 --- a/apps/recorder/src/extract/mikan/web.rs +++ b/apps/recorder/src/extract/mikan/web.rs @@ -801,11 +801,6 @@ pub async fn scrape_mikan_poster_meta_from_image_url( .write(storage_path.clone(), poster_data) .await?; - tracing::warn!( - poster_str = poster_str.to_string(), - "mikan poster meta extracted" - ); - MikanBangumiPosterMeta { origin_poster_src: origin_poster_src_url, poster_src: Some(poster_str.to_string()), diff --git a/apps/recorder/src/lib.rs b/apps/recorder/src/lib.rs index aea74cc..a7c02d7 100644 --- a/apps/recorder/src/lib.rs +++ b/apps/recorder/src/lib.rs @@ -7,7 +7,8 @@ async_fn_traits, error_generic_member_access, associated_type_defaults, - let_chains + let_chains, + impl_trait_in_fn_trait_return )] #![allow(clippy::enum_variant_names)] pub use downloader; diff --git a/apps/recorder/src/web/controller/core.rs b/apps/recorder/src/web/controller/core.rs index 9fa9634..9dde999 100644 --- a/apps/recorder/src/web/controller/core.rs +++ b/apps/recorder/src/web/controller/core.rs @@ -9,12 +9,12 @@ pub trait ControllerTrait: Sized { -> Router>; } -pub struct PrefixController { +pub struct NestRouterController { prefix: Cow<'static, str>, router: Router>, } -impl PrefixController { +impl NestRouterController { pub fn new( prefix: impl Into>, router: Router>, @@ -26,7 +26,7 @@ impl PrefixController { } } -impl ControllerTrait for PrefixController { +impl ControllerTrait for NestRouterController { fn apply_to( self, router: Router>, @@ -36,15 +36,15 @@ impl ControllerTrait for PrefixController { } pub enum Controller { - Prefix(PrefixController), + NestRouter(NestRouterController), } impl Controller { - pub fn from_prefix( + pub fn from_nest_router( prefix: impl Into>, router: Router>, ) -> Self { - Self::Prefix(PrefixController::new(prefix, router)) + Self::NestRouter(NestRouterController::new(prefix, router)) } } @@ -54,7 +54,7 @@ impl ControllerTrait for Controller { router: Router>, ) -> Router> { match self { - Self::Prefix(p) => p.apply_to(router), + Self::NestRouter(p) => p.apply_to(router), } } } diff --git a/apps/recorder/src/web/controller/feeds/mod.rs b/apps/recorder/src/web/controller/feeds/mod.rs index 6d3cb0a..d8d38c2 100644 --- a/apps/recorder/src/web/controller/feeds/mod.rs +++ b/apps/recorder/src/web/controller/feeds/mod.rs @@ -38,5 +38,5 @@ async fn rss_handler( pub async fn create(_ctx: Arc) -> RecorderResult { let router = Router::>::new().route("/rss/{token}", get(rss_handler)); - Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) + Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router)) } diff --git a/apps/recorder/src/web/controller/graphql/mod.rs b/apps/recorder/src/web/controller/graphql/mod.rs index 96fde93..fd47db3 100644 --- a/apps/recorder/src/web/controller/graphql/mod.rs +++ b/apps/recorder/src/web/controller/graphql/mod.rs @@ -71,5 +71,5 @@ pub async fn create(ctx: Arc) -> RecorderResult post(graphql_handler).layer(from_fn_with_state(ctx, auth_middleware)), ) .route("/introspection", introspection_handler); - Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) + Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router)) } diff --git a/apps/recorder/src/web/controller/metadata/mod.rs b/apps/recorder/src/web/controller/metadata/mod.rs index e7c56d3..b59d597 100644 --- a/apps/recorder/src/web/controller/metadata/mod.rs +++ b/apps/recorder/src/web/controller/metadata/mod.rs @@ -38,5 +38,5 @@ pub async fn create(_context: Arc) -> RecorderResult) -> RecorderResult) -> RecorderResult ) .route("/public/{*path}", get(serve_public_static)); - Ok(Controller::from_prefix(CONTROLLER_PREFIX, router)) + Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router)) } diff --git a/apps/webui/.env b/apps/webui/.env.development similarity index 100% rename from apps/webui/.env rename to apps/webui/.env.development diff --git a/apps/webui/.env.production.example b/apps/webui/.env.production.example new file mode 100644 index 0000000..85fb74b --- /dev/null +++ b/apps/webui/.env.production.example @@ -0,0 +1,6 @@ +AUTH_TYPE = "basic" # or oidc +# OIDC_ISSUER="https://auth.logto.io/oidc" +# OIDC_AUDIENCE = "https://konobangu.com/api" +# OIDC_CLIENT_ID = "client_id" +# OIDC_CLIENT_SECRET = "client_secret" # optional +# OIDC_EXTRA_SCOPES = "read:konobangu write:konobangu" diff --git a/justfile b/justfile index 34beb15..1ef8eeb 100644 --- a/justfile +++ b/justfile @@ -4,7 +4,7 @@ set dotenv-load := true prepare-dev: cargo install cargo-binstall cargo binstall sea-orm-cli cargo-llvm-cov cargo-nextest - # install watchexec just zellij nasm libjxl + # install watchexec just zellij nasm libjxl netcat prepare-dev-testcontainers: docker pull linuxserver/qbittorrent:latest @@ -17,6 +17,11 @@ dev-optimize-images: dev-webui: pnpm run --filter=webui dev +prod-webui: + pnpm run --filter=webui build + mkdir -p apps/recorder/webui + cp -r apps/webui/dist/* apps/recorder/webui/ + dev-proxy: npx --yes kill-port --port 8899,5005 pnpm run --parallel --filter=proxy dev @@ -24,6 +29,9 @@ dev-proxy: dev-recorder: watchexec -r -e rs,toml,yaml,json,env -- cargo run -p recorder --bin recorder_cli -- --environment=development --graceful-shutdown=false +prod-recorder: prod-webui + cargo run --release -p recorder --bin recorder_cli -- --environment=production --working-dir=apps/recorder --graceful-shutdown=false + dev-recorder-migrate-down: cargo run -p recorder --bin migrate_down -- --environment development