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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AuthUserPayload { pub id: String, pub public_user_code: String, pub username: String, pub display_name: String, pub avatar_url: Option, pub phone_number_masked: Option, pub login_method: String, pub binding_status: String, pub wechat_bound: bool, pub created_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PublicUserSummaryPayload { pub id: String, pub public_user_code: String, pub display_name: String, pub avatar_url: Option, } #[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 { pub phone: 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, 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, pub avatar_data_url: Option, } #[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 { pub phone: 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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AuthSessionsResponse { pub sessions: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AuthSessionSummaryPayload { pub session_id: String, pub session_ids: Vec, pub session_count: u32, pub client_type: String, pub client_runtime: String, pub client_platform: String, pub client_label: String, pub device_display_name: String, pub mini_program_app_id: Option, pub mini_program_env: Option, pub user_agent: Option, pub ip_masked: Option, 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 { pub phone: String, pub scene: Option, } #[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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PhoneLoginRequest { pub phone: String, pub code: String, #[serde(default)] pub invite_code: Option, } #[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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct PhoneLoginReferralResponse { pub ok: bool, pub message: Option, pub invitee_reward_granted: bool, pub inviter_reward_granted: bool, pub invitee_balance_after: Option, pub inviter_balance_after: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WechatStartQuery { pub redirect_path: Option, } #[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, pub code: Option, pub mock_code: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct WechatBindPhoneRequest { #[serde(default)] pub phone: Option, #[serde(default)] pub code: Option, #[serde(default)] pub wechat_phone_code: Option, } #[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, } #[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, wechat_auth_enabled: bool, ) -> Vec { 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 { phone: "13800138000".to_string(), password: "secret123".to_string(), }) .expect("payload should serialize"); assert_eq!( payload, json!({ "phone": "13800138000", "password": "secret123" }) ); } #[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 { phone: None, code: None, wechat_phone_code: Some("wx-phone-code-001".to_string()), }) .expect("payload should serialize"); assert_eq!( payload, json!({ "phone": null, "code": null, "wechatPhoneCode": "wx-phone-code-001" }) ); } }