44748b7846
来自今早群友反映 before:  and failed after:    * https://developers.weixin.qq.com/miniprogram/dev/server/API/user-info/phone-number/api_getphonenumber.html#Res-phone-info-Object-Payload 参考微信这个文档修改了微信返回的struct 手机号 区域码字段不应是Optional * 登录注册改密码改绑定等 api 把单一 phone字段改成 country_code(可选,缺省为86) + pure_phone_number 分别进行了前后端校验 Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/104 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
443 lines
13 KiB
Rust
443 lines
13 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
pub const AUTH_LOGIN_METHOD_PASSWORD: &str = "password";
|
|
pub const AUTH_LOGIN_METHOD_PHONE: &str = "phone";
|
|
pub const AUTH_LOGIN_METHOD_WECHAT: &str = "wechat";
|
|
pub const AUTH_BINDING_STATUS_ACTIVE: &str = "active";
|
|
pub const AUTH_BINDING_STATUS_PENDING_BIND_PHONE: &str = "pending_bind_phone";
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AuthLoginOptionsResponse {
|
|
pub available_login_methods: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AuthUserPayload {
|
|
pub id: String,
|
|
pub public_user_code: String,
|
|
pub display_name: String,
|
|
pub avatar_url: Option<String>,
|
|
pub phone_number: Option<String>,
|
|
pub phone_number_masked: Option<String>,
|
|
pub login_method: String,
|
|
pub binding_status: String,
|
|
pub wechat_bound: bool,
|
|
pub wechat_display_name: Option<String>,
|
|
pub wechat_account: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PublicUserSummaryPayload {
|
|
pub id: String,
|
|
pub public_user_code: String,
|
|
pub username: String,
|
|
pub display_name: String,
|
|
pub avatar_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PublicUserSearchResponse {
|
|
pub user: PublicUserSummaryPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordEntryRequest {
|
|
#[serde(default)]
|
|
pub country_code: Option<String>,
|
|
pub pure_phone_number: String,
|
|
pub password: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordEntryResponse {
|
|
pub token: String,
|
|
pub user: AuthUserPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordChangeRequest {
|
|
pub current_password: Option<String>,
|
|
pub new_password: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordChangeResponse {
|
|
pub user: AuthUserPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProfileUpdateRequest {
|
|
pub display_name: Option<String>,
|
|
pub avatar_data_url: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ProfileUpdateResponse {
|
|
pub user: AuthUserPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordResetRequest {
|
|
#[serde(default)]
|
|
pub country_code: Option<String>,
|
|
pub pure_phone_number: String,
|
|
pub code: String,
|
|
pub new_password: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PasswordResetResponse {
|
|
pub token: String,
|
|
pub user: AuthUserPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AuthMeResponse {
|
|
pub user: AuthUserPayload,
|
|
pub available_login_methods: Vec<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AuthSessionsResponse {
|
|
pub sessions: Vec<AuthSessionSummaryPayload>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AuthSessionSummaryPayload {
|
|
pub session_id: String,
|
|
pub session_ids: Vec<String>,
|
|
pub session_count: u32,
|
|
pub client_label: String,
|
|
pub ip_masked: Option<String>,
|
|
pub is_current: bool,
|
|
pub created_at: String,
|
|
pub last_seen_at: String,
|
|
pub expires_at: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct RefreshSessionResponse {
|
|
pub token: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct LogoutResponse {
|
|
pub ok: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct LogoutAllResponse {
|
|
pub ok: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct RevokeAuthSessionResponse {
|
|
pub ok: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PhoneSendCodeRequest {
|
|
#[serde(default)]
|
|
pub country_code: Option<String>,
|
|
pub pure_phone_number: String,
|
|
pub scene: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PhoneSendCodeResponse {
|
|
pub ok: bool,
|
|
pub cooldown_seconds: u64,
|
|
pub expires_in_seconds: u64,
|
|
pub provider_request_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PhoneLoginRequest {
|
|
#[serde(default)]
|
|
pub country_code: Option<String>,
|
|
pub pure_phone_number: String,
|
|
pub code: String,
|
|
#[serde(default)]
|
|
pub invite_code: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PhoneLoginResponse {
|
|
pub token: String,
|
|
pub user: AuthUserPayload,
|
|
pub created: bool,
|
|
pub referral: Option<PhoneLoginReferralResponse>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PhoneLoginReferralResponse {
|
|
pub ok: bool,
|
|
pub message: Option<String>,
|
|
pub invitee_reward_granted: bool,
|
|
pub inviter_reward_granted: bool,
|
|
pub invitee_balance_after: Option<u64>,
|
|
pub inviter_balance_after: Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WechatStartQuery {
|
|
pub redirect_path: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WechatStartResponse {
|
|
pub authorization_url: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct WechatCallbackQuery {
|
|
pub state: Option<String>,
|
|
pub code: Option<String>,
|
|
pub mock_code: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WechatBindPhoneRequest {
|
|
#[serde(default)]
|
|
pub country_code: Option<String>,
|
|
#[serde(default)]
|
|
pub pure_phone_number: Option<String>,
|
|
#[serde(default)]
|
|
pub code: Option<String>,
|
|
#[serde(default)]
|
|
pub wechat_phone_code: Option<String>,
|
|
#[serde(default)]
|
|
pub display_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WechatBindPhoneResponse {
|
|
pub token: String,
|
|
pub user: AuthUserPayload,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct WechatMiniProgramLoginRequest {
|
|
pub code: String,
|
|
#[serde(default)]
|
|
pub display_name: Option<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 created: bool,
|
|
}
|
|
|
|
pub fn build_available_login_methods(
|
|
sms_auth_enabled: bool,
|
|
password_auth_enabled: bool,
|
|
wechat_auth_enabled: bool,
|
|
) -> Vec<String> {
|
|
let mut methods = Vec::new();
|
|
if sms_auth_enabled {
|
|
methods.push(AUTH_LOGIN_METHOD_PHONE.to_string());
|
|
}
|
|
if password_auth_enabled {
|
|
methods.push(AUTH_LOGIN_METHOD_PASSWORD.to_string());
|
|
}
|
|
if wechat_auth_enabled {
|
|
methods.push(AUTH_LOGIN_METHOD_WECHAT.to_string());
|
|
}
|
|
methods
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn available_login_methods_keep_phone_password_wechat_order() {
|
|
let methods = build_available_login_methods(true, true, true);
|
|
|
|
assert_eq!(
|
|
methods,
|
|
vec![
|
|
AUTH_LOGIN_METHOD_PHONE.to_string(),
|
|
AUTH_LOGIN_METHOD_PASSWORD.to_string(),
|
|
AUTH_LOGIN_METHOD_WECHAT.to_string()
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn available_login_methods_keep_password_as_default_entry() {
|
|
let methods = build_available_login_methods(false, true, false);
|
|
|
|
assert_eq!(methods, vec![AUTH_LOGIN_METHOD_PASSWORD.to_string()]);
|
|
}
|
|
|
|
#[test]
|
|
fn password_entry_request_uses_camel_case_fields() {
|
|
let payload = serde_json::to_value(PasswordEntryRequest {
|
|
country_code: Some("86".to_string()),
|
|
pure_phone_number: "13800138000".to_string(),
|
|
password: "secret123".to_string(),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(
|
|
payload,
|
|
json!({
|
|
"countryCode": "86",
|
|
"purePhoneNumber": "13800138000",
|
|
"password": "secret123"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn password_entry_request_defaults_country_code_and_rejects_legacy_phone_field() {
|
|
let request = serde_json::from_value::<PasswordEntryRequest>(json!({
|
|
"purePhoneNumber": "13800138000",
|
|
"password": "secret123"
|
|
}))
|
|
.expect("country code should be optional");
|
|
assert_eq!(request.country_code, None);
|
|
|
|
let legacy = serde_json::from_value::<PasswordEntryRequest>(json!({
|
|
"phone": "13800138000",
|
|
"password": "secret123"
|
|
}));
|
|
assert!(
|
|
legacy.is_err(),
|
|
"legacy phone field must not satisfy request"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn profile_update_request_uses_camel_case_fields() {
|
|
let payload = serde_json::to_value(ProfileUpdateRequest {
|
|
display_name: Some("旅人甲".to_string()),
|
|
avatar_data_url: Some("data:image/png;base64,AAAA".to_string()),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(
|
|
payload,
|
|
json!({
|
|
"displayName": "旅人甲",
|
|
"avatarDataUrl": "data:image/png;base64,AAAA"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wechat_callback_query_keeps_provider_compatible_field_names() {
|
|
let payload = serde_json::to_value(WechatCallbackQuery {
|
|
state: Some("state-1".to_string()),
|
|
code: Some("code-1".to_string()),
|
|
mock_code: Some("mock-1".to_string()),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(
|
|
payload,
|
|
json!({
|
|
"state": "state-1",
|
|
"code": "code-1",
|
|
"mock_code": "mock-1"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wechat_bind_phone_request_accepts_mini_program_phone_code() {
|
|
let payload = serde_json::to_value(WechatBindPhoneRequest {
|
|
country_code: None,
|
|
pure_phone_number: None,
|
|
code: None,
|
|
wechat_phone_code: Some("wx-phone-code-001".to_string()),
|
|
display_name: Some("陶泥儿玩家".to_string()),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(
|
|
payload,
|
|
json!({
|
|
"countryCode": null,
|
|
"purePhoneNumber": null,
|
|
"code": null,
|
|
"wechatPhoneCode": "wx-phone-code-001",
|
|
"displayName": "陶泥儿玩家"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wechat_mini_program_login_request_accepts_native_nickname() {
|
|
let payload = serde_json::to_value(WechatMiniProgramLoginRequest {
|
|
code: "wx-mini-code-001".to_string(),
|
|
display_name: Some("陶泥儿玩家".to_string()),
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(
|
|
payload,
|
|
json!({
|
|
"code": "wx-mini-code-001",
|
|
"displayName": "陶泥儿玩家"
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wechat_mini_program_login_response_marks_created_user() {
|
|
let payload = serde_json::to_value(WechatMiniProgramLoginResponse {
|
|
token: "token-001".to_string(),
|
|
binding_status: AUTH_BINDING_STATUS_PENDING_BIND_PHONE.to_string(),
|
|
user: AuthUserPayload {
|
|
id: "user_001".to_string(),
|
|
public_user_code: "SY-00000001".to_string(),
|
|
display_name: "微信旅人".to_string(),
|
|
avatar_url: None,
|
|
phone_number: None,
|
|
phone_number_masked: None,
|
|
login_method: AUTH_LOGIN_METHOD_WECHAT.to_string(),
|
|
binding_status: AUTH_BINDING_STATUS_PENDING_BIND_PHONE.to_string(),
|
|
wechat_bound: true,
|
|
wechat_display_name: None,
|
|
wechat_account: Some("wx-openid-001".to_string()),
|
|
},
|
|
created: true,
|
|
})
|
|
.expect("payload should serialize");
|
|
|
|
assert_eq!(payload["created"], serde_json::Value::Bool(true));
|
|
}
|
|
}
|