diff --git a/server-rs/crates/api-server/src/auth_session.rs b/server-rs/crates/api-server/src/auth_session.rs index 90b1325e2..1e0be778f 100644 --- a/server-rs/crates/api-server/src/auth_session.rs +++ b/server-rs/crates/api-server/src/auth_session.rs @@ -21,12 +21,26 @@ pub struct SignedAuthSession { pub refresh_token: String, } -pub fn create_password_auth_session( +/// 创建登录会话并立即同步认证投影。 +/// +/// `create_auth_session` 只更新 api-server 进程内的认证工作集。登录响应返回前必须 +/// 完成一次投影同步,否则客户端拿到的 access token 会在下一次请求经过 +/// `validate_auth_session` 时被其它节点或当前节点的 SpacetimeDB 校验拒绝。 +pub async fn create_auth_session_and_sync( state: &AppState, user: &AuthUser, session_client: &SessionClientContext, + session_provider: AuthLoginMethod, ) -> Result { - create_auth_session(state, user, session_client, AuthLoginMethod::Password) + let signed_session = create_auth_session(state, user, session_client, session_provider)?; + state + .sync_auth_store_tables_to_spacetime() + .await + .map_err(|error| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR) + .with_message(format!("同步认证状态失败:{error}")) + })?; + Ok(signed_session) } #[cfg(not(test))] @@ -193,3 +207,48 @@ fn map_access_token_device_info(client_info: &RefreshSessionClientInfo) -> Acces client_platform: client_info.client_platform.clone(), } } + +#[cfg(test)] +mod tests { + use axum::http::HeaderMap; + + use super::*; + use crate::{config::AppConfig, session_client::resolve_session_client_context}; + + #[tokio::test] + async fn create_auth_session_and_sync_persists_the_new_session_projection() { + let state = AppState::new(AppConfig::default()).expect("state should build"); + let user = state + .seed_test_phone_user_with_password("13800138099", "secret123") + .await; + let session_client = resolve_session_client_context(&HeaderMap::new()); + + assert!( + !state.test_auth_projection_is_synced(), + "the seeded user is a local change until the login path syncs it" + ); + + let signed_session = + create_auth_session_and_sync(&state, &user, &session_client, AuthLoginMethod::Password) + .await + .expect("auth session should be created and synchronized"); + + assert!(state.test_auth_projection_is_synced()); + let claims = platform_auth::verify_access_token( + &signed_session.access_token, + state.auth_jwt_config(), + ) + .expect("access token should be valid"); + assert_eq!(claims.user_id(), user.id); + assert!( + state + .refresh_session_service() + .is_session_active_for_user( + claims.user_id(), + claims.session_id(), + time::OffsetDateTime::now_utc(), + ) + .expect("session state should be readable") + ); + } +} diff --git a/server-rs/crates/api-server/src/password_entry.rs b/server-rs/crates/api-server/src/password_entry.rs index 2c0549a9f..f76b83079 100644 --- a/server-rs/crates/api-server/src/password_entry.rs +++ b/server-rs/crates/api-server/src/password_entry.rs @@ -13,7 +13,7 @@ use crate::{ auth_payload::map_auth_user_payload, auth_session::{ attach_set_cookie_header, build_refresh_session_cookie_header, - create_password_auth_session, record_daily_login_tracking_event_after_auth_success, + create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, request_context::RequestContext, @@ -80,7 +80,13 @@ pub async fn password_entry( return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) .with_message("账号服务暂不可用,请稍后重试")); } - let signed_session = create_password_auth_session(&state, &result.user, &session_client)?; + let signed_session = create_auth_session_and_sync( + &state, + &result.user, + &session_client, + AuthLoginMethod::Password, + ) + .await?; record_daily_login_tracking_event_after_auth_success( &state, &request_context, diff --git a/server-rs/crates/api-server/src/password_management.rs b/server-rs/crates/api-server/src/password_management.rs index d0cd01046..4f51ea71a 100644 --- a/server-rs/crates/api-server/src/password_management.rs +++ b/server-rs/crates/api-server/src/password_management.rs @@ -17,7 +17,7 @@ use crate::{ auth_payload::map_auth_user_payload, auth_session::{ attach_set_cookie_header, build_clear_refresh_session_cookie_header, - build_refresh_session_cookie_header, create_auth_session, + build_refresh_session_cookie_header, create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, @@ -139,19 +139,13 @@ pub async fn reset_password( return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) .with_message("账号服务暂不可用,请稍后重试")); } - let signed_session = create_auth_session( + let signed_session = create_auth_session_and_sync( &state, &result.user, &session_client, module_auth::AuthLoginMethod::Password, - )?; - state - .sync_auth_store_tables_to_spacetime() - .await - .map_err(|error| { - AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR) - .with_message(format!("同步认证状态失败:{error}")) - })?; + ) + .await?; record_daily_login_tracking_event_after_auth_success( &state, &request_context, diff --git a/server-rs/crates/api-server/src/phone_auth.rs b/server-rs/crates/api-server/src/phone_auth.rs index 5580e386a..227870794 100644 --- a/server-rs/crates/api-server/src/phone_auth.rs +++ b/server-rs/crates/api-server/src/phone_auth.rs @@ -19,8 +19,8 @@ use crate::{ api_response::json_success_body, auth_payload::map_auth_user_payload, auth_session::{ - attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session, - record_daily_login_tracking_event_after_auth_success, + attach_set_cookie_header, build_refresh_session_cookie_header, + create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, platform_errors::{attach_retry_after, map_phone_auth_platform_store_error}, @@ -256,12 +256,13 @@ pub async fn phone_login( return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) .with_message("账号服务暂不可用,请稍后重试")); } - let signed_session = create_auth_session( + let signed_session = create_auth_session_and_sync( &state, &result.user, &session_client, AuthLoginMethod::Phone, - )?; + ) + .await?; record_daily_login_tracking_event_after_auth_success( &state, &request_context, diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 284b437cb..dc53af8cc 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -1309,7 +1309,11 @@ impl AppState { pub async fn sync_auth_store_tables_to_spacetime(&self) -> Result<(), SpacetimeClientError> { #[cfg(test)] - return Ok(()); + { + self.auth_projection_synced_revision + .store(self.auth_store.revision(), Ordering::Release); + return Ok(()); + } #[cfg(not(test))] let _sync_guard = self.auth_projection_sync_lock.lock().await; @@ -1769,6 +1773,10 @@ fn creation_entry_feature_gates_require_user_tags( #[cfg(test)] impl AppState { + pub(crate) fn test_auth_projection_is_synced(&self) -> bool { + self.auth_projection_synced_revision.load(Ordering::Acquire) == self.auth_store.revision() + } + pub(crate) fn seed_test_refresh_session_for_user( &self, user: &module_auth::AuthUser, diff --git a/server-rs/crates/api-server/src/wechat/auth.rs b/server-rs/crates/api-server/src/wechat/auth.rs index 08f012bd7..884911515 100644 --- a/server-rs/crates/api-server/src/wechat/auth.rs +++ b/server-rs/crates/api-server/src/wechat/auth.rs @@ -24,8 +24,8 @@ use crate::{ auth::AuthenticatedAccessToken, auth_payload::map_auth_user_payload, auth_session::{ - attach_set_cookie_header, build_refresh_session_cookie_header, create_auth_session, - record_daily_login_tracking_event_after_auth_success, + attach_set_cookie_header, build_refresh_session_cookie_header, + create_auth_session_and_sync, record_daily_login_tracking_event_after_auth_success, }, http_error::AppError, platform_errors::{attach_retry_after, map_wechat_provider_error}, @@ -318,12 +318,13 @@ pub async fn handle_wechat_callback( )) .into_response()); } - let signed_session = create_auth_session( + let signed_session = create_auth_session_and_sync( &state, &result.user, &session_client, session_provider.clone(), - )?; + ) + .await?; record_daily_login_tracking_event_after_auth_success( &state, &request_context, @@ -451,12 +452,13 @@ pub async fn bind_wechat_phone( return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) .with_message("账号服务暂不可用,请稍后重试")); } - let signed_session = create_auth_session( + let signed_session = create_auth_session_and_sync( &state, &result.user, &session_client, AuthLoginMethod::Wechat, - )?; + ) + .await?; if result.activated_new_user { crate::registration_reward::grant_new_user_registration_wallet_reward( &state, @@ -550,12 +552,13 @@ pub async fn login_wechat_mini_program( return Err(AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) .with_message("账号服务暂不可用,请稍后重试")); } - let signed_session = create_auth_session( + let signed_session = create_auth_session_and_sync( &state, &result.user, &session_client, AuthLoginMethod::Wechat, - )?; + ) + .await?; let mut response_headers = HeaderMap::new(); attach_set_cookie_header(