fix: fix production issues

This commit is contained in:
master 2025-06-25 05:21:08 +08:00
parent 571caf50ff
commit 41ff5c2a11
23 changed files with 109 additions and 50 deletions

View File

@ -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"

View File

@ -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"

View File

@ -29,3 +29,5 @@ temp/*
!temp/.gitkeep
tests/resources/mikan/classic_episodes/*/*
!tests/resources/mikan/classic_episodes/parquet/tiny.parquet
webui/
data/

View File

@ -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",

View File

@ -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

View File

@ -72,6 +72,11 @@ impl AppBuilder {
}
pub async fn build(self) -> RecorderResult<App> {
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<AppConfig> {
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"

View File

@ -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<AppConfig> {
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;
}
}

View File

@ -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() {

View File

@ -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<Arc<dyn AppContextTrait>>,
@ -38,3 +41,37 @@ pub async fn auth_middleware(
response
}
pub async fn webui_auth_middleware(
State(ctx): State<Arc<dyn AppContextTrait>>,
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
}
}

View File

@ -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};

View File

@ -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())
}

View File

@ -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()),

View File

@ -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;

View File

@ -9,12 +9,12 @@ pub trait ControllerTrait: Sized {
-> Router<Arc<dyn AppContextTrait>>;
}
pub struct PrefixController {
pub struct NestRouterController {
prefix: Cow<'static, str>,
router: Router<Arc<dyn AppContextTrait>>,
}
impl PrefixController {
impl NestRouterController {
pub fn new(
prefix: impl Into<Cow<'static, str>>,
router: Router<Arc<dyn AppContextTrait>>,
@ -26,7 +26,7 @@ impl PrefixController {
}
}
impl ControllerTrait for PrefixController {
impl ControllerTrait for NestRouterController {
fn apply_to(
self,
router: Router<Arc<dyn AppContextTrait>>,
@ -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<Cow<'static, str>>,
router: Router<Arc<dyn AppContextTrait>>,
) -> Self {
Self::Prefix(PrefixController::new(prefix, router))
Self::NestRouter(NestRouterController::new(prefix, router))
}
}
@ -54,7 +54,7 @@ impl ControllerTrait for Controller {
router: Router<Arc<dyn AppContextTrait>>,
) -> Router<Arc<dyn AppContextTrait>> {
match self {
Self::Prefix(p) => p.apply_to(router),
Self::NestRouter(p) => p.apply_to(router),
}
}
}

View File

@ -38,5 +38,5 @@ async fn rss_handler(
pub async fn create(_ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller> {
let router = Router::<Arc<dyn AppContextTrait>>::new().route("/rss/{token}", get(rss_handler));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
}

View File

@ -71,5 +71,5 @@ pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller>
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))
}

View File

@ -38,5 +38,5 @@ pub async fn create(_context: Arc<dyn AppContextTrait>) -> RecorderResult<Contro
.route("/health", get(health))
.route("/ping", get(ping));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
}

View File

@ -5,4 +5,4 @@ pub mod metadata;
pub mod oidc;
pub mod r#static;
pub use core::{Controller, ControllerTrait, PrefixController};
pub use core::{Controller, ControllerTrait, NestRouterController};

View File

@ -77,5 +77,5 @@ pub async fn create(_context: Arc<dyn AppContextTrait>) -> RecorderResult<Contro
.route("/auth", get(oidc_auth))
.route("/callback", get(oidc_callback));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
}

View File

@ -99,5 +99,5 @@ pub async fn create(ctx: Arc<dyn AppContextTrait>) -> RecorderResult<Controller>
)
.route("/public/{*path}", get(serve_public_static));
Ok(Controller::from_prefix(CONTROLLER_PREFIX, router))
Ok(Controller::from_nest_router(CONTROLLER_PREFIX, router))
}

View File

@ -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"

View File

@ -4,7 +4,7 @@ set dotenv-load := true
prepare-dev:
cargo install cargo-binstall
cargo binstall sea-orm-cli cargo-llvm-cov cargo-nextest
# <package-manager> install watchexec just zellij nasm libjxl
# <package-manager> 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