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::{ AuthUserPayload, PhoneLoginRequest, PhoneLoginResponse, PhoneSendCodeRequest, PhoneSendCodeResponse, }; use time::OffsetDateTime; use crate::{ api_response::json_success_body, 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 result = state .phone_auth_service() .send_code( SendPhoneCodeInput { phone_number: payload.phone, scene, }, OffsetDateTime::now_utc(), ) .map_err(map_phone_auth_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 = state .phone_auth_service() .login( PhoneLoginInput { phone_number: payload.phone, verify_code: payload.code, }, OffsetDateTime::now_utc(), ) .await .map_err(map_phone_auth_error)?; let session_client = resolve_session_client_context(&headers); let signed_session = create_auth_session( &state, &result.user, &session_client, AuthLoginMethod::Phone, )?; 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: AuthUserPayload { id: result.user.id, username: result.user.username, display_name: result.user.display_name, phone_number_masked: result.user.phone_number_masked, login_method: result.user.login_method.as_str().to_string(), binding_status: result.user.binding_status.as_str().to_string(), wechat_bound: result.user.wechat_bound, }, }, ), )) } 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), _ => Err(AppError::from_status(StatusCode::BAD_REQUEST) .with_message("短信验证码场景不合法") .with_details(json!({ "field": "scene" }))), } } 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()) } } }