feat: add wechat miniprogram webview login
This commit is contained in:
@@ -123,7 +123,9 @@ use crate::{
|
||||
begin_story_runtime_session, begin_story_session, continue_story,
|
||||
get_story_runtime_projection, get_story_session_state, resolve_story_runtime_action,
|
||||
},
|
||||
wechat_auth::{bind_wechat_phone, handle_wechat_callback, start_wechat_login},
|
||||
wechat_auth::{
|
||||
bind_wechat_phone, handle_wechat_callback, login_wechat_mini_program, start_wechat_login,
|
||||
},
|
||||
};
|
||||
|
||||
const PUZZLE_REFERENCE_IMAGE_BODY_LIMIT_BYTES: usize = 12 * 1024 * 1024;
|
||||
@@ -241,6 +243,10 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.route("/api/auth/phone/login", post(phone_login))
|
||||
.route("/api/auth/wechat/start", get(start_wechat_login))
|
||||
.route("/api/auth/wechat/callback", get(handle_wechat_callback))
|
||||
.route(
|
||||
"/api/auth/wechat/miniprogram-login",
|
||||
post(login_wechat_mini_program),
|
||||
)
|
||||
.route(
|
||||
"/api/auth/wechat/bind-phone",
|
||||
post(bind_wechat_phone).route_layer(middleware::from_fn_with_state(
|
||||
@@ -2716,6 +2722,103 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wechat_miniprogram_login_returns_system_token_and_marks_session_source() {
|
||||
let config = AppConfig {
|
||||
wechat_auth_enabled: true,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/wechat/miniprogram-login")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-client-type", "mini_program")
|
||||
.header("x-client-runtime", "wechat_mini_program")
|
||||
.header("x-client-platform", "ios")
|
||||
.header("x-client-instance-id", "mini-instance-001")
|
||||
.header("x-mini-program-app-id", "wx-mini-test")
|
||||
.header("x-mini-program-env", "develop")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"code": "wx-mini-code-001"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("mini program login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("mini program login request should succeed");
|
||||
|
||||
assert_eq!(login_response.status(), StatusCode::OK);
|
||||
let refresh_cookie = login_response
|
||||
.headers()
|
||||
.get("set-cookie")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.expect("refresh cookie should exist")
|
||||
.to_string();
|
||||
let login_body = login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("mini program login body should collect")
|
||||
.to_bytes();
|
||||
let login_payload: Value =
|
||||
serde_json::from_slice(&login_body).expect("mini program login payload should be json");
|
||||
let token = login_payload["token"]
|
||||
.as_str()
|
||||
.expect("system token should exist")
|
||||
.to_string();
|
||||
|
||||
assert_eq!(
|
||||
login_payload["bindingStatus"],
|
||||
Value::String("pending_bind_phone".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
login_payload["user"]["loginMethod"],
|
||||
Value::String("wechat".to_string())
|
||||
);
|
||||
assert!(refresh_cookie.contains("genarrative_refresh_session="));
|
||||
|
||||
let sessions_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/sessions")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.header("cookie", refresh_cookie)
|
||||
.body(Body::empty())
|
||||
.expect("sessions request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("sessions request should succeed");
|
||||
|
||||
assert_eq!(sessions_response.status(), StatusCode::OK);
|
||||
let sessions_body = sessions_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("sessions body should collect")
|
||||
.to_bytes();
|
||||
let sessions_payload: Value =
|
||||
serde_json::from_slice(&sessions_body).expect("sessions payload should be json");
|
||||
assert_eq!(
|
||||
sessions_payload["sessions"][0]["clientType"],
|
||||
Value::String("mini_program".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
sessions_payload["sessions"][0]["clientRuntime"],
|
||||
Value::String("wechat_mini_program".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
sessions_payload["sessions"][0]["miniProgramAppId"],
|
||||
Value::String("wx-mini-test".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wechat_bind_phone_merges_into_existing_phone_user() {
|
||||
let config = AppConfig {
|
||||
|
||||
@@ -52,11 +52,14 @@ pub struct AppConfig {
|
||||
pub wechat_auth_provider: String,
|
||||
pub wechat_app_id: Option<String>,
|
||||
pub wechat_app_secret: Option<String>,
|
||||
pub wechat_mini_program_app_id: Option<String>,
|
||||
pub wechat_mini_program_app_secret: Option<String>,
|
||||
pub wechat_callback_path: String,
|
||||
pub wechat_redirect_path: String,
|
||||
pub wechat_authorize_endpoint: String,
|
||||
pub wechat_access_token_endpoint: String,
|
||||
pub wechat_user_info_endpoint: String,
|
||||
pub wechat_js_code_session_endpoint: String,
|
||||
pub wechat_state_ttl_minutes: u32,
|
||||
pub wechat_mock_user_id: String,
|
||||
pub wechat_mock_union_id: Option<String>,
|
||||
@@ -146,12 +149,16 @@ impl Default for AppConfig {
|
||||
wechat_auth_provider: "mock".to_string(),
|
||||
wechat_app_id: None,
|
||||
wechat_app_secret: None,
|
||||
wechat_mini_program_app_id: None,
|
||||
wechat_mini_program_app_secret: None,
|
||||
wechat_callback_path: "/api/auth/wechat/callback".to_string(),
|
||||
wechat_redirect_path: "/".to_string(),
|
||||
wechat_authorize_endpoint: "https://open.weixin.qq.com/connect/qrconnect".to_string(),
|
||||
wechat_access_token_endpoint: "https://api.weixin.qq.com/sns/oauth2/access_token"
|
||||
.to_string(),
|
||||
wechat_user_info_endpoint: "https://api.weixin.qq.com/sns/userinfo".to_string(),
|
||||
wechat_js_code_session_endpoint: "https://api.weixin.qq.com/sns/jscode2session"
|
||||
.to_string(),
|
||||
wechat_state_ttl_minutes: 15,
|
||||
wechat_mock_user_id: "wx-mock-user".to_string(),
|
||||
wechat_mock_union_id: Some("wx-mock-union".to_string()),
|
||||
@@ -355,6 +362,10 @@ impl AppConfig {
|
||||
}
|
||||
config.wechat_app_id = read_first_non_empty_env(&["WECHAT_APP_ID"]);
|
||||
config.wechat_app_secret = read_first_non_empty_env(&["WECHAT_APP_SECRET"]);
|
||||
config.wechat_mini_program_app_id =
|
||||
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_APP_ID", "WECHAT_APP_ID"]);
|
||||
config.wechat_mini_program_app_secret =
|
||||
read_first_non_empty_env(&["WECHAT_MINI_PROGRAM_APP_SECRET", "WECHAT_APP_SECRET"]);
|
||||
if let Some(wechat_callback_path) = read_first_non_empty_env(&["WECHAT_CALLBACK_PATH"]) {
|
||||
config.wechat_callback_path = wechat_callback_path;
|
||||
}
|
||||
@@ -376,6 +387,11 @@ impl AppConfig {
|
||||
{
|
||||
config.wechat_user_info_endpoint = wechat_user_info_endpoint;
|
||||
}
|
||||
if let Some(wechat_js_code_session_endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_JS_CODE_SESSION_ENDPOINT"])
|
||||
{
|
||||
config.wechat_js_code_session_endpoint = wechat_js_code_session_endpoint;
|
||||
}
|
||||
if let Some(wechat_state_ttl_minutes) =
|
||||
read_first_positive_u32_env(&["WECHAT_STATE_TTL_MINUTES"])
|
||||
{
|
||||
|
||||
@@ -9,7 +9,8 @@ use module_auth::{
|
||||
};
|
||||
use platform_auth::WechatAuthScene;
|
||||
use shared_contracts::auth::{
|
||||
WechatBindPhoneRequest, WechatBindPhoneResponse, WechatCallbackQuery, WechatStartQuery,
|
||||
WechatBindPhoneRequest, WechatBindPhoneResponse, WechatCallbackQuery,
|
||||
WechatMiniProgramLoginRequest, WechatMiniProgramLoginResponse, WechatStartQuery,
|
||||
WechatStartResponse,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
@@ -234,6 +235,68 @@ pub async fn bind_wechat_phone(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn login_wechat_mini_program(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<WechatMiniProgramLoginRequest>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
if !state.config.wechat_auth_enabled {
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信登录暂未启用"));
|
||||
}
|
||||
let code = payload.code.trim();
|
||||
if code.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少微信授权 code")
|
||||
);
|
||||
}
|
||||
|
||||
let profile = state
|
||||
.wechat_provider()
|
||||
.resolve_mini_program_login_profile(Some(code))
|
||||
.await
|
||||
.map_err(map_wechat_provider_error)?;
|
||||
let result = state
|
||||
.wechat_auth_service()
|
||||
.resolve_login(module_auth::ResolveWechatLoginInput {
|
||||
profile: map_wechat_profile_to_domain(profile),
|
||||
})
|
||||
.await
|
||||
.map_err(map_wechat_auth_error)?;
|
||||
let session_client = resolve_session_client_context(&headers);
|
||||
let signed_session = create_auth_session(
|
||||
&state,
|
||||
&result.user,
|
||||
&session_client,
|
||||
AuthLoginMethod::Wechat,
|
||||
)?;
|
||||
state
|
||||
.sync_auth_store_snapshot_to_spacetime()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.with_message(format!("同步认证快照失败:{error}"))
|
||||
})?;
|
||||
|
||||
let mut response_headers = HeaderMap::new();
|
||||
attach_set_cookie_header(
|
||||
&mut response_headers,
|
||||
build_refresh_session_cookie_header(&state, &signed_session.refresh_token)?,
|
||||
);
|
||||
|
||||
Ok((
|
||||
response_headers,
|
||||
json_success_body(
|
||||
Some(&request_context),
|
||||
WechatMiniProgramLoginResponse {
|
||||
token: signed_session.access_token,
|
||||
binding_status: result.user.binding_status.as_str().to_string(),
|
||||
user: map_auth_user_payload(result.user),
|
||||
},
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_wechat_scene(user_agent: Option<&str>) -> Result<WechatAuthScene, AppError> {
|
||||
let user_agent = user_agent.unwrap_or_default();
|
||||
let is_wechat = user_agent.contains("MicroMessenger");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use platform_auth::{
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_AUTHORIZE_ENDPOINT,
|
||||
DEFAULT_WECHAT_USER_INFO_ENDPOINT, WechatAuthConfig, WechatProvider,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_USER_INFO_ENDPOINT, WechatAuthConfig,
|
||||
WechatProvider,
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
@@ -11,6 +12,8 @@ pub fn build_wechat_provider(config: &AppConfig) -> WechatProvider {
|
||||
config.wechat_auth_provider.clone(),
|
||||
config.wechat_app_id.clone(),
|
||||
config.wechat_app_secret.clone(),
|
||||
config.wechat_mini_program_app_id.clone(),
|
||||
config.wechat_mini_program_app_secret.clone(),
|
||||
normalize_wechat_endpoint(
|
||||
&config.wechat_authorize_endpoint,
|
||||
DEFAULT_WECHAT_AUTHORIZE_ENDPOINT,
|
||||
@@ -23,6 +26,10 @@ pub fn build_wechat_provider(config: &AppConfig) -> WechatProvider {
|
||||
&config.wechat_user_info_endpoint,
|
||||
DEFAULT_WECHAT_USER_INFO_ENDPOINT,
|
||||
),
|
||||
normalize_wechat_endpoint(
|
||||
&config.wechat_js_code_session_endpoint,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT,
|
||||
),
|
||||
config.wechat_mock_user_id.clone(),
|
||||
config.wechat_mock_union_id.clone(),
|
||||
config.wechat_mock_display_name.clone(),
|
||||
|
||||
@@ -42,6 +42,8 @@ pub const DEFAULT_WECHAT_IN_APP_AUTHORIZE_ENDPOINT: &str =
|
||||
pub const DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT: &str =
|
||||
"https://api.weixin.qq.com/sns/oauth2/access_token";
|
||||
pub const DEFAULT_WECHAT_USER_INFO_ENDPOINT: &str = "https://api.weixin.qq.com/sns/userinfo";
|
||||
pub const DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT: &str =
|
||||
"https://api.weixin.qq.com/sns/jscode2session";
|
||||
|
||||
type HmacSha1 = Hmac<Sha1>;
|
||||
|
||||
@@ -178,9 +180,12 @@ pub struct WechatAuthConfig {
|
||||
pub provider: String,
|
||||
pub app_id: Option<String>,
|
||||
pub app_secret: Option<String>,
|
||||
pub mini_program_app_id: Option<String>,
|
||||
pub mini_program_app_secret: Option<String>,
|
||||
pub authorize_endpoint: String,
|
||||
pub access_token_endpoint: String,
|
||||
pub user_info_endpoint: String,
|
||||
pub js_code_session_endpoint: String,
|
||||
pub mock_user_id: String,
|
||||
pub mock_union_id: Option<String>,
|
||||
pub mock_display_name: String,
|
||||
@@ -213,11 +218,14 @@ pub struct MockWechatProvider {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RealWechatProvider {
|
||||
client: Client,
|
||||
app_id: String,
|
||||
app_secret: String,
|
||||
app_id: Option<String>,
|
||||
app_secret: Option<String>,
|
||||
mini_program_app_id: Option<String>,
|
||||
mini_program_app_secret: Option<String>,
|
||||
authorize_endpoint: String,
|
||||
access_token_endpoint: String,
|
||||
user_info_endpoint: String,
|
||||
js_code_session_endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -311,6 +319,14 @@ struct WechatUserInfoResponse {
|
||||
errmsg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WechatJsCodeSessionResponse {
|
||||
openid: Option<String>,
|
||||
unionid: Option<String>,
|
||||
errcode: Option<i64>,
|
||||
errmsg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AliyunSendSmsVerifyCodeResponse {
|
||||
// 阿里云 RPC 原始 JSON 使用首字母大写字段名,这里必须显式映射,避免把成功响应误判成空值。
|
||||
@@ -628,9 +644,12 @@ impl WechatAuthConfig {
|
||||
provider: String,
|
||||
app_id: Option<String>,
|
||||
app_secret: Option<String>,
|
||||
mini_program_app_id: Option<String>,
|
||||
mini_program_app_secret: Option<String>,
|
||||
authorize_endpoint: String,
|
||||
access_token_endpoint: String,
|
||||
user_info_endpoint: String,
|
||||
js_code_session_endpoint: String,
|
||||
mock_user_id: String,
|
||||
mock_union_id: Option<String>,
|
||||
mock_display_name: String,
|
||||
@@ -641,9 +660,12 @@ impl WechatAuthConfig {
|
||||
provider,
|
||||
app_id,
|
||||
app_secret,
|
||||
mini_program_app_id,
|
||||
mini_program_app_secret,
|
||||
authorize_endpoint,
|
||||
access_token_endpoint,
|
||||
user_info_endpoint,
|
||||
js_code_session_endpoint,
|
||||
mock_user_id,
|
||||
mock_union_id,
|
||||
mock_display_name,
|
||||
@@ -667,20 +689,36 @@ impl WechatProvider {
|
||||
});
|
||||
}
|
||||
|
||||
let Some(app_id) = config.app_id else {
|
||||
return Self::Disabled;
|
||||
};
|
||||
let Some(app_secret) = config.app_secret else {
|
||||
let has_web_oauth_config = config
|
||||
.app_id
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
&& config
|
||||
.app_secret
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_mini_program_config = config
|
||||
.mini_program_app_id
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
&& config
|
||||
.mini_program_app_secret
|
||||
.as_ref()
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
if !has_web_oauth_config && !has_mini_program_config {
|
||||
return Self::Disabled;
|
||||
};
|
||||
|
||||
Self::Real(RealWechatProvider {
|
||||
client: Client::new(),
|
||||
app_id,
|
||||
app_secret,
|
||||
app_id: config.app_id,
|
||||
app_secret: config.app_secret,
|
||||
mini_program_app_id: config.mini_program_app_id,
|
||||
mini_program_app_secret: config.mini_program_app_secret,
|
||||
authorize_endpoint: config.authorize_endpoint,
|
||||
access_token_endpoint: config.access_token_endpoint,
|
||||
user_info_endpoint: config.user_info_endpoint,
|
||||
js_code_session_endpoint: config.js_code_session_endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -708,6 +746,17 @@ impl WechatProvider {
|
||||
Self::Real(provider) => provider.resolve_callback_profile(code).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_mini_program_login_profile(
|
||||
&self,
|
||||
code: Option<&str>,
|
||||
) -> Result<WechatIdentityProfile, WechatProviderError> {
|
||||
match self {
|
||||
Self::Disabled => Err(WechatProviderError::Disabled),
|
||||
Self::Mock(provider) => Ok(provider.resolve_callback_profile(code)),
|
||||
Self::Real(provider) => provider.resolve_mini_program_login_profile(code).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockWechatProvider {
|
||||
@@ -740,8 +789,11 @@ impl RealWechatProvider {
|
||||
let mut url = Url::parse(endpoint).map_err(|error| {
|
||||
WechatProviderError::InvalidConfig(format!("微信授权地址非法:{error}"))
|
||||
})?;
|
||||
let app_id = self.app_id.as_ref().ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信开放平台 AppID 未配置".to_string())
|
||||
})?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("appid", &self.app_id)
|
||||
.append_pair("appid", app_id)
|
||||
.append_pair("redirect_uri", callback_url)
|
||||
.append_pair("response_type", "code")
|
||||
.append_pair(
|
||||
@@ -764,13 +816,19 @@ impl RealWechatProvider {
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or(WechatProviderError::MissingCode)?;
|
||||
|
||||
let app_id = self.app_id.as_ref().ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信开放平台 AppID 未配置".to_string())
|
||||
})?;
|
||||
let app_secret = self.app_secret.as_ref().ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信开放平台 AppSecret 未配置".to_string())
|
||||
})?;
|
||||
let mut access_token_url = Url::parse(&self.access_token_endpoint).map_err(|error| {
|
||||
WechatProviderError::InvalidConfig(format!("微信 access_token 地址非法:{error}"))
|
||||
})?;
|
||||
access_token_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("appid", &self.app_id)
|
||||
.append_pair("secret", &self.app_secret)
|
||||
.append_pair("appid", app_id)
|
||||
.append_pair("secret", app_secret)
|
||||
.append_pair("code", code)
|
||||
.append_pair("grant_type", "authorization_code");
|
||||
|
||||
@@ -856,6 +914,84 @@ impl RealWechatProvider {
|
||||
avatar_url: user_info_payload.headimgurl,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_mini_program_login_profile(
|
||||
&self,
|
||||
code: Option<&str>,
|
||||
) -> Result<WechatIdentityProfile, WechatProviderError> {
|
||||
let code = code
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or(WechatProviderError::MissingCode)?;
|
||||
let app_id = self
|
||||
.mini_program_app_id
|
||||
.as_ref()
|
||||
.or(self.app_id.as_ref())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信小程序 AppID 未配置".to_string())
|
||||
})?;
|
||||
let app_secret = self
|
||||
.mini_program_app_secret
|
||||
.as_ref()
|
||||
.or(self.app_secret.as_ref())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信小程序 AppSecret 未配置".to_string())
|
||||
})?;
|
||||
|
||||
let mut js_code_session_url =
|
||||
Url::parse(&self.js_code_session_endpoint).map_err(|error| {
|
||||
WechatProviderError::InvalidConfig(format!("微信 jscode2session 地址非法:{error}"))
|
||||
})?;
|
||||
js_code_session_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("appid", app_id)
|
||||
.append_pair("secret", app_secret)
|
||||
.append_pair("js_code", code)
|
||||
.append_pair("grant_type", "authorization_code");
|
||||
|
||||
let payload = self
|
||||
.client
|
||||
.get(js_code_session_url.as_str())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
warn!(error = %error, "微信小程序 jscode2session 请求失败");
|
||||
WechatProviderError::RequestFailed(
|
||||
"微信小程序登录失败:jscode2session 请求失败".to_string(),
|
||||
)
|
||||
})?
|
||||
.json::<WechatJsCodeSessionResponse>()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
warn!(error = %error, "微信小程序 jscode2session 响应解析失败");
|
||||
WechatProviderError::DeserializeFailed(
|
||||
"微信小程序登录失败:jscode2session 响应非法".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(errcode) = payload.errcode.filter(|value| *value != 0) {
|
||||
return Err(WechatProviderError::Upstream(format!(
|
||||
"微信小程序登录失败:{}",
|
||||
payload
|
||||
.errmsg
|
||||
.unwrap_or_else(|| format!("jscode2session 返回错误 {errcode}"))
|
||||
)));
|
||||
}
|
||||
|
||||
let provider_uid = payload
|
||||
.openid
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::MissingProfile("微信小程序登录失败:缺少 openid".to_string())
|
||||
})?;
|
||||
|
||||
Ok(WechatIdentityProfile {
|
||||
provider_uid,
|
||||
provider_union_id: payload.unionid,
|
||||
display_name: None,
|
||||
avatar_url: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mock_wechat_authorization_url(
|
||||
@@ -1723,9 +1859,12 @@ mod tests {
|
||||
"mock".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
DEFAULT_WECHAT_AUTHORIZE_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_USER_INFO_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT.to_string(),
|
||||
"wx-user-001".to_string(),
|
||||
Some("wx-union-001".to_string()),
|
||||
"微信测试用户".to_string(),
|
||||
@@ -1751,9 +1890,12 @@ mod tests {
|
||||
"mock".to_string(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
DEFAULT_WECHAT_AUTHORIZE_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_USER_INFO_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT.to_string(),
|
||||
"wx-user-001".to_string(),
|
||||
Some("wx-union-001".to_string()),
|
||||
"微信测试用户".to_string(),
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
7. `auth/wechat/start`
|
||||
8. `auth/wechat/callback`
|
||||
9. `auth/wechat/bind-phone`
|
||||
10. `auth/wechat/miniprogram-login`
|
||||
|
||||
当前阶段继续补齐的 Stage3 公开请求 DTO:
|
||||
|
||||
|
||||
@@ -222,6 +222,20 @@ pub struct WechatBindPhoneResponse {
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatMiniProgramLoginRequest {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatMiniProgramLoginResponse {
|
||||
pub token: String,
|
||||
pub binding_status: String,
|
||||
pub user: AuthUserPayload,
|
||||
}
|
||||
|
||||
pub fn build_available_login_methods(
|
||||
sms_auth_enabled: bool,
|
||||
password_auth_enabled: bool,
|
||||
|
||||
Reference in New Issue
Block a user