feat: support mini program phone authorization binding
This commit is contained in:
@@ -3801,6 +3801,113 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wechat_miniprogram_bind_phone_code_activates_pending_user() {
|
||||
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-bind-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-bind-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 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())
|
||||
);
|
||||
|
||||
let bind_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/wechat/bind-phone")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.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-bind-instance-001")
|
||||
.header("x-mini-program-app-id", "wx-mini-test")
|
||||
.header("x-mini-program-env", "develop")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"wechatPhoneCode": "13800138000"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("bind request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("bind request should succeed");
|
||||
|
||||
assert_eq!(bind_response.status(), StatusCode::OK);
|
||||
assert!(
|
||||
bind_response
|
||||
.headers()
|
||||
.get("set-cookie")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value.contains("genarrative_refresh_session="))
|
||||
);
|
||||
let bind_body = bind_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("bind body should collect")
|
||||
.to_bytes();
|
||||
let bind_payload: Value =
|
||||
serde_json::from_slice(&bind_body).expect("bind payload should be json");
|
||||
|
||||
assert_eq!(
|
||||
bind_payload["user"]["bindingStatus"],
|
||||
Value::String("active".to_string())
|
||||
);
|
||||
assert_eq!(bind_payload["user"]["wechatBound"], Value::Bool(true));
|
||||
assert_eq!(
|
||||
bind_payload["user"]["phoneNumberMasked"],
|
||||
Value::String("138****8000".to_string())
|
||||
);
|
||||
assert!(
|
||||
bind_payload["token"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wechat_bind_phone_merges_into_existing_phone_user() {
|
||||
let config = AppConfig {
|
||||
|
||||
@@ -64,6 +64,8 @@ pub struct AppConfig {
|
||||
pub wechat_access_token_endpoint: String,
|
||||
pub wechat_user_info_endpoint: String,
|
||||
pub wechat_js_code_session_endpoint: String,
|
||||
pub wechat_stable_access_token_endpoint: String,
|
||||
pub wechat_phone_number_endpoint: String,
|
||||
pub wechat_state_ttl_minutes: u32,
|
||||
pub wechat_mock_user_id: String,
|
||||
pub wechat_mock_union_id: Option<String>,
|
||||
@@ -178,6 +180,10 @@ impl Default for AppConfig {
|
||||
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_stable_access_token_endpoint: "https://api.weixin.qq.com/cgi-bin/stable_token"
|
||||
.to_string(),
|
||||
wechat_phone_number_endpoint:
|
||||
"https://api.weixin.qq.com/wxa/business/getuserphonenumber".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()),
|
||||
@@ -426,6 +432,16 @@ impl AppConfig {
|
||||
{
|
||||
config.wechat_js_code_session_endpoint = wechat_js_code_session_endpoint;
|
||||
}
|
||||
if let Some(wechat_stable_access_token_endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT"])
|
||||
{
|
||||
config.wechat_stable_access_token_endpoint = wechat_stable_access_token_endpoint;
|
||||
}
|
||||
if let Some(wechat_phone_number_endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_PHONE_NUMBER_ENDPOINT"])
|
||||
{
|
||||
config.wechat_phone_number_endpoint = wechat_phone_number_endpoint;
|
||||
}
|
||||
if let Some(wechat_state_ttl_minutes) =
|
||||
read_first_positive_u32_env(&["WECHAT_STATE_TTL_MINUTES"])
|
||||
{
|
||||
|
||||
@@ -5,7 +5,8 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use module_auth::{
|
||||
AuthLoginMethod, BindWechatPhoneInput, CreateWechatAuthStateInput, WechatAuthError,
|
||||
AuthLoginMethod, BindWechatPhoneInput, BindWechatVerifiedPhoneInput,
|
||||
CreateWechatAuthStateInput, WechatAuthError,
|
||||
};
|
||||
use platform_auth::WechatAuthScene;
|
||||
use shared_contracts::auth::{
|
||||
@@ -191,18 +192,55 @@ pub async fn bind_wechat_phone(
|
||||
if !state.config.wechat_auth_enabled {
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信登录暂未启用"));
|
||||
}
|
||||
let result = state
|
||||
.phone_auth_service()
|
||||
.bind_wechat_phone(
|
||||
BindWechatPhoneInput {
|
||||
let result = if let Some(wechat_phone_code) = payload
|
||||
.wechat_phone_code
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let phone_profile = state
|
||||
.wechat_provider()
|
||||
.resolve_mini_program_phone_number(Some(wechat_phone_code))
|
||||
.await
|
||||
.map_err(map_wechat_provider_error)?;
|
||||
state
|
||||
.phone_auth_service()
|
||||
.bind_wechat_verified_phone(BindWechatVerifiedPhoneInput {
|
||||
user_id: authenticated.claims().user_id().to_string(),
|
||||
phone_number: payload.phone,
|
||||
verify_code: payload.code,
|
||||
},
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_wechat_bind_phone_error)?;
|
||||
phone_number: phone_profile.phone_number,
|
||||
})
|
||||
.await
|
||||
.map_err(map_wechat_bind_phone_error)?
|
||||
} else {
|
||||
let phone = payload
|
||||
.phone
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少需要绑定的手机号")
|
||||
})?;
|
||||
let code = payload
|
||||
.code
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少短信验证码")
|
||||
})?;
|
||||
state
|
||||
.phone_auth_service()
|
||||
.bind_wechat_phone(
|
||||
BindWechatPhoneInput {
|
||||
user_id: authenticated.claims().user_id().to_string(),
|
||||
phone_number: phone.to_string(),
|
||||
verify_code: code.to_string(),
|
||||
},
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_wechat_bind_phone_error)?
|
||||
};
|
||||
if result.activated_new_user {
|
||||
crate::registration_reward::grant_new_user_registration_wallet_reward(
|
||||
&state,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use platform_auth::{
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_AUTHORIZE_ENDPOINT,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_USER_INFO_ENDPOINT, WechatAuthConfig,
|
||||
WechatProvider,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT,
|
||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_USER_INFO_ENDPOINT,
|
||||
WechatAuthConfig, WechatProvider,
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
@@ -30,6 +31,14 @@ pub fn build_wechat_provider(config: &AppConfig) -> WechatProvider {
|
||||
&config.wechat_js_code_session_endpoint,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT,
|
||||
),
|
||||
normalize_wechat_endpoint(
|
||||
&config.wechat_stable_access_token_endpoint,
|
||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT,
|
||||
),
|
||||
normalize_wechat_endpoint(
|
||||
&config.wechat_phone_number_endpoint,
|
||||
DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT,
|
||||
),
|
||||
config.wechat_mock_user_id.clone(),
|
||||
config.wechat_mock_union_id.clone(),
|
||||
config.wechat_mock_display_name.clone(),
|
||||
|
||||
Reference in New Issue
Block a user