Files
Genarrative/server-rs/crates/shared-contracts/src/auth.rs
2026-05-01 20:29:09 +08:00

322 lines
9.0 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 username: String,
pub display_name: String,
pub avatar_url: Option<String>,
pub phone_number_masked: Option<String>,
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<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 {
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<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 {
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<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 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<String>,
pub mini_program_env: Option<String>,
pub user_agent: Option<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)]
#[serde(rename_all = "camelCase")]
pub struct PhoneSendCodeRequest {
pub phone: 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 {
pub phone: 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 {
pub phone: String,
pub code: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WechatBindPhoneResponse {
pub token: String,
pub user: AuthUserPayload,
}
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 {
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"
})
);
}
}