use axum::{ Json, extract::{Extension, State}, http::{HeaderMap, HeaderValue, StatusCode}, response::IntoResponse, }; use module_auth::{ AuthLoginMethod, PhoneAuthError, PhoneAuthScene, PhoneLoginInput, SendPhoneCodeInput, }; use serde_json::json; use shared_contracts::auth::{ PhoneLoginRequest, PhoneLoginResponse, PhoneSendCodeRequest, PhoneSendCodeResponse, }; use time::OffsetDateTime; use tracing::{info, warn}; 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, }, http_error::AppError, request_context::RequestContext, session_client::resolve_session_client_context, state::AppState, }; pub async fn send_phone_code( State(state): State, Extension(request_context): Extension, Json(payload): Json, ) -> Result, AppError> { // 短信登录开关由服务端配置统一控制,避免前端误调用未开放能力。 if !state.config.sms_auth_enabled { return Err( AppError::from_status(StatusCode::BAD_REQUEST).with_message("手机号登录暂未启用") ); } let scene = map_phone_auth_scene(payload.scene.as_deref())?; let phone_input_masked = mask_phone_input(payload.phone.as_str()); info!( request_id = request_context.request_id(), operation = request_context.operation(), scene = scene.as_str(), provider = state.config.sms_auth_provider.as_str(), phone_input_masked = phone_input_masked.as_str(), "收到手机号验证码发送请求" ); let result = match state .phone_auth_service() .send_code( SendPhoneCodeInput { phone_number: payload.phone, scene: scene.clone(), }, OffsetDateTime::now_utc(), ) .await { Ok(result) => { info!( request_id = request_context.request_id(), operation = request_context.operation(), scene = %result.scene, phone_masked = %result.phone_number_masked, provider = %result.provider, provider_request_id = %result.provider_request_id.as_deref().unwrap_or("unknown"), provider_out_id = %result.provider_out_id.as_deref().unwrap_or("unknown"), cooldown_seconds = result.cooldown_seconds, expires_in_seconds = result.expires_in_seconds, "手机号验证码发送请求已提交" ); result } Err(error) => { warn!( request_id = request_context.request_id(), operation = request_context.operation(), scene = scene.as_str(), provider = state.config.sms_auth_provider.as_str(), phone_input_masked = phone_input_masked.as_str(), error = %error, "手机号验证码发送失败" ); return Err(map_phone_auth_error(error)); } }; Ok(json_success_body( Some(&request_context), PhoneSendCodeResponse { ok: true, cooldown_seconds: result.cooldown_seconds, expires_in_seconds: result.expires_in_seconds, provider_request_id: result.provider_request_id, }, )) } pub async fn phone_login( State(state): State, Extension(request_context): Extension, headers: HeaderMap, Json(payload): Json, ) -> Result { // 手机号验证码校验通过后,沿用统一会话签发逻辑,确保 refresh cookie 与 JWT 行为一致。 if !state.config.sms_auth_enabled { return Err( AppError::from_status(StatusCode::BAD_REQUEST).with_message("手机号登录暂未启用") ); } let result = match state .phone_auth_service() .login( PhoneLoginInput { phone_number: payload.phone, verify_code: payload.code, }, OffsetDateTime::now_utc(), ) .await { Ok(result) => { info!( request_id = request_context.request_id(), operation = request_context.operation(), scene = "login", phone_masked = %result.phone_number_masked, provider = %result.provider, provider_out_id = %result.provider_out_id.as_deref().unwrap_or("unknown"), user_id = %result.user.id, created = result.created, "手机号验证码登录成功" ); result } Err(error) => { warn!( request_id = request_context.request_id(), operation = request_context.operation(), scene = "login", error = %error, "手机号验证码登录失败" ); return Err(map_phone_auth_error(error)); } }; let session_client = resolve_session_client_context(&headers); let signed_session = create_auth_session( &state, &result.user, &session_client, AuthLoginMethod::Phone, )?; state .sync_auth_store_snapshot_to_spacetime() .await .map_err(|error| { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR) .with_message(format!("同步认证快照失败:{error}")) })?; let mut headers = HeaderMap::new(); attach_set_cookie_header( &mut headers, build_refresh_session_cookie_header(&state, &signed_session.refresh_token)?, ); Ok(( headers, json_success_body( Some(&request_context), PhoneLoginResponse { token: signed_session.access_token, user: map_auth_user_payload(result.user), }, ), )) } fn map_phone_auth_scene(raw_scene: Option<&str>) -> Result { match raw_scene.unwrap_or("login").trim() { "login" => Ok(PhoneAuthScene::Login), "bind_phone" => Ok(PhoneAuthScene::BindPhone), "change_phone" => Ok(PhoneAuthScene::ChangePhone), "reset_password" => Ok(PhoneAuthScene::ResetPassword), _ => Err(AppError::from_status(StatusCode::BAD_REQUEST) .with_message("短信验证码场景不合法") .with_details(json!({ "field": "scene" }))), } } fn mask_phone_input(phone: &str) -> String { let trimmed = phone.trim(); if trimmed.is_empty() { return "empty".to_string(); } let digits: String = trimmed.chars().filter(|ch| ch.is_ascii_digit()).collect(); let target = if digits.len() >= 7 { digits } else { trimmed.to_string() }; mask_phone_digits(&target) } fn mask_phone_digits(value: &str) -> String { let chars: Vec = value.chars().collect(); if chars.len() <= 4 { return "*".repeat(chars.len().max(1)); } let prefix_len = chars.len().min(3); let suffix_len = 4.min(chars.len().saturating_sub(prefix_len)); let mask_len = chars.len().saturating_sub(prefix_len + suffix_len); let mut masked = String::new(); masked.extend(chars.iter().take(prefix_len)); masked.push_str(&"*".repeat(mask_len.max(1))); if suffix_len > 0 { masked.extend(chars.iter().skip(chars.len() - suffix_len)); } masked } pub fn map_phone_auth_error(error: PhoneAuthError) -> AppError { match error { PhoneAuthError::InvalidPhoneNumber | PhoneAuthError::InvalidVerifyCode | PhoneAuthError::VerifyCodeNotFound | PhoneAuthError::VerifyCodeExpired | PhoneAuthError::UserStateMismatch => { AppError::from_status(StatusCode::BAD_REQUEST).with_message(error.to_string()) } PhoneAuthError::SendCoolingDown { retry_after_seconds, } => { let app_error = AppError::from_status(StatusCode::TOO_MANY_REQUESTS) .with_message(error.to_string()) .with_details(json!({ "retryAfterSeconds": retry_after_seconds })); match HeaderValue::from_str(&retry_after_seconds.to_string()) { Ok(value) => app_error.with_header("retry-after", value), Err(_) => app_error, } } PhoneAuthError::VerifyAttemptsExceeded => { AppError::from_status(StatusCode::TOO_MANY_REQUESTS).with_message(error.to_string()) } PhoneAuthError::UserNotFound => { AppError::from_status(StatusCode::UNAUTHORIZED).with_message(error.to_string()) } PhoneAuthError::Store(_) | PhoneAuthError::PasswordHash(_) => { AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_message(error.to_string()) } } }