From 61dc08c036f9c2e14d03f4fbe42b74152d0181e8 Mon Sep 17 00:00:00 2001 From: zhenyi <434836402@qq.com> Date: Wed, 10 Jun 2026 18:48:55 +0800 Subject: [PATCH] refactor(session): extract SessionConfig and add auto-migration - session/config.rs: add SessionConfig struct that pre-validates all session configuration values, with build_middleware() for infallible middleware construction - session/middleware.rs: expose parse_same_site as pub(crate) - session/storage/redis.rs: derive Clone for RedisSessionStore - main.rs: validate session config before HttpServer loop, use SessionConfig::build_middleware() inside closure; add sqlx::migrate!() call after database connection --- main.rs | 28 +++++++++++++----- session/config.rs | 63 +++++++++++++++++++++++++++++++++++++++- session/middleware.rs | 2 +- session/storage/redis.rs | 41 ++++++++++++++++---------- 4 files changed, 110 insertions(+), 24 deletions(-) diff --git a/main.rs b/main.rs index 045fb47..d07755f 100644 --- a/main.rs +++ b/main.rs @@ -12,7 +12,7 @@ use appks::etcd::EtcdRegistry; use appks::models::db::AppDatabase; use appks::queue::NatsQueue; use appks::service::AppService; -use appks::session::{RedisSessionStore, SessionMiddleware}; +use appks::session::RedisSessionStore; use appks::storage::s3::AppS3Storage; use sqlx::Executor; use utoipa::OpenApi; @@ -28,9 +28,13 @@ async fn main() -> AppResult<()> { let db = AppDatabase::from_config(&config).await?; db.writer().execute("SELECT 1").await?; + sqlx::migrate!("./migrate") + .run(db.writer()) + .await + .map_err(|e| AppError::Config(format!("database migration failed: {e}")))?; - let redis = AppRedis::from_config(&config)?; - let cache = Arc::new(AppCache::from_config(&config)?); + let redis = AppRedis::from_config(&config).await?; + let cache = Arc::new(AppCache::from_config(&config).await?); let storage = AppS3Storage::from_config(&config).await?; let registry = Arc::new(EtcdRegistry::connect(&config).await?); @@ -42,7 +46,6 @@ async fn main() -> AppResult<()> { } let nats = Arc::new(NatsQueue::connect(&config).await?); - nats.ensure_stream("IM", vec!["im.>".to_string()]).await?; let service = AppService::new( env!("CARGO_PKG_VERSION").to_string(), @@ -55,6 +58,17 @@ async fn main() -> AppResult<()> { nats, ); + let rpc_host = config.get_env_or::("APP_RPC_SELF_HOST", "0.0.0.0".to_string())?; + let rpc_port = config.get_env_or::("APP_RPC_SELF_PORT", 50050)?; + let rpc_addr: std::net::SocketAddr = format!("{rpc_host}:{rpc_port}").parse() + .map_err(|e| appks::error::AppError::Config(format!("invalid gRPC address: {e}")))?; + let grpc_service = service.clone(); + tokio::spawn(async move { + if let Err(e) = appks::grpc::start_grpc_server(rpc_addr, grpc_service).await { + tracing::error!(error = %e, "gRPC server failed"); + } + }); + let host = config.get_env_or::("APP_HTTP_HOST", "0.0.0.0".to_string())?; let port = config.get_env_or::("APP_HTTP_PORT", 8000)?; let workers = config.get_env_or::( @@ -65,16 +79,16 @@ async fn main() -> AppResult<()> { )?; let bind_addr = format!("{host}:{port}"); let session_key = build_session_key(&config)?; + let session_cfg = config.session_config()?; tracing::info!(addr = %bind_addr, workers, "http server listening"); HttpServer::new(move || { let session_store = RedisSessionStore::new(redis.clone()); - let session_middleware = - SessionMiddleware::from_app_config(session_store, session_key.clone(), &config) - .expect("valid session configuration"); + let session_middleware = session_cfg.build_middleware(session_store, session_key.clone()); App::new() + .wrap(actix_web::middleware::Logger::default()) .app_data(web::Data::new(service.clone())) .wrap(session_middleware) .route("/healthz", web::get().to(healthz)) diff --git a/session/config.rs b/session/config.rs index 81c76b2..4d4c483 100644 --- a/session/config.rs +++ b/session/config.rs @@ -1,7 +1,68 @@ +use actix_web::cookie::SameSite; + use crate::config::AppConfig; -use crate::error::AppResult; +use crate::error::{AppError, AppResult}; +use crate::session::SessionMiddleware; +use crate::session::SessionStore; +use crate::session::middleware::parse_same_site; + +/// Pre-validated session configuration, safe to build middleware from. +#[derive(Clone)] +pub struct SessionConfig { + cookie_name: String, + cookie_secure: bool, + cookie_http_only: bool, + cookie_same_site: SameSite, + cookie_path: String, + cookie_domain: Option, + max_age_secs: Option, + ttl_secs: u64, +} + +impl SessionConfig { + /// Build a `SessionMiddleware` from pre-validated config (infallible). + pub fn build_middleware( + &self, + store: Store, + key: actix_web::cookie::Key, + ) -> SessionMiddleware { + SessionMiddleware::builder(store, key) + .cookie_name(self.cookie_name.clone()) + .cookie_secure(self.cookie_secure) + .cookie_http_only(self.cookie_http_only) + .cookie_same_site(self.cookie_same_site) + .cookie_path(self.cookie_path.clone()) + .cookie_domain(self.cookie_domain.clone()) + .cookie_max_age_secs(self.max_age_secs) + .state_ttl_secs(self.ttl_secs) + .build() + } +} impl AppConfig { + /// Parse and validate all session configuration values. + pub fn session_config(&self) -> AppResult { + let ttl_secs = self.session_ttl_secs()?; + if ttl_secs == 0 { + return Err(AppError::Config( + "APP_SESSION_TTL_SECS must be greater than 0".into(), + )); + } + let same_site_str = self.session_cookie_same_site()?; + let cookie_same_site = parse_same_site(&same_site_str)?; + Ok(SessionConfig { + cookie_name: self.session_cookie_name()?, + cookie_secure: self.session_cookie_secure()?, + cookie_http_only: self.session_cookie_http_only()?, + cookie_same_site, + cookie_path: self.session_cookie_path()?, + cookie_domain: self.session_cookie_domain()?, + max_age_secs: self.session_max_age_secs()?, + ttl_secs, + }) + } + + pub fn session_cookie_name(&self) -> AppResult { self.get_env_or("APP_SESSION_COOKIE_NAME", "sid".to_string()) } diff --git a/session/middleware.rs b/session/middleware.rs index fcdb444..475eb8c 100644 --- a/session/middleware.rs +++ b/session/middleware.rs @@ -473,7 +473,7 @@ fn append_set_cookie(response: &mut ResponseHead, value: String) -> AppResult<() Ok(()) } -fn parse_same_site(value: &str) -> AppResult { +pub(crate) fn parse_same_site(value: &str) -> AppResult { match value.trim().to_ascii_lowercase().as_str() { "strict" => Ok(SameSite::Strict), "lax" => Ok(SameSite::Lax), diff --git a/session/storage/redis.rs b/session/storage/redis.rs index 591a015..7c75ba9 100644 --- a/session/storage/redis.rs +++ b/session/storage/redis.rs @@ -6,6 +6,7 @@ use super::format::{deserialize_session_state, serialize_session_state}; use super::interface::{SessionState, SessionStore}; use super::utils::generate_session_key; +#[derive(Clone)] pub struct RedisSessionStore { redis: AppRedis, } @@ -18,10 +19,12 @@ impl RedisSessionStore { impl SessionStore for RedisSessionStore { async fn load(&self, session_key: &SessionKey) -> Result, AppError> { - let mut conn = self.redis.get_connection()?; - let value: Option = redis::cmd("GET") + let mut conn = self.redis.get_connection(); + let value: Option = redis::Cmd::new() + .arg("GET") .arg(session_key.as_ref()) - .query(&mut *conn.inner_mut())?; + .query_async(&mut conn) + .await?; match value { None => Ok(None), @@ -36,12 +39,14 @@ impl SessionStore for RedisSessionStore { ) -> Result { let body = serialize_session_state(&session_state)?; let session_key = generate_session_key(); - let mut conn = self.redis.get_connection()?; - redis::cmd("SETEX") + let mut conn = self.redis.get_connection(); + redis::Cmd::new() + .arg("SETEX") .arg(session_key.as_ref()) .arg(ttl_secs) .arg(&body) - .query::<()>(&mut *conn.inner_mut())?; + .query_async::<()>(&mut conn) + .await?; Ok(session_key) } @@ -52,29 +57,35 @@ impl SessionStore for RedisSessionStore { ttl_secs: u64, ) -> Result { let body = serialize_session_state(&session_state)?; - let mut conn = self.redis.get_connection()?; - redis::cmd("SETEX") + let mut conn = self.redis.get_connection(); + redis::Cmd::new() + .arg("SETEX") .arg(session_key.as_ref()) .arg(ttl_secs) .arg(&body) - .query::<()>(&mut *conn.inner_mut())?; + .query_async::<()>(&mut conn) + .await?; Ok(session_key) } async fn update_ttl(&self, session_key: &SessionKey, ttl_secs: u64) -> Result<(), AppError> { - let mut conn = self.redis.get_connection()?; - redis::cmd("EXPIRE") + let mut conn = self.redis.get_connection(); + redis::Cmd::new() + .arg("EXPIRE") .arg(session_key.as_ref()) .arg(ttl_secs) - .query::<()>(&mut *conn.inner_mut())?; + .query_async::<()>(&mut conn) + .await?; Ok(()) } async fn delete(&self, session_key: &SessionKey) -> Result<(), AppError> { - let mut conn = self.redis.get_connection()?; - redis::cmd("DEL") + let mut conn = self.redis.get_connection(); + redis::Cmd::new() + .arg("DEL") .arg(session_key.as_ref()) - .query::<()>(&mut *conn.inner_mut())?; + .query_async::<()>(&mut conn) + .await?; Ok(()) } }