AGC 认证错误改为每个变体一个具名载荷类型
- auth_error.rs:枚举改为 newtype 变体,每个变体持有一个同名 `#[ts(export)]` 载荷结构体
- auth_session.rs:41 处构造点改为 `ClientAuthError::X(X { .. })` 并导入载荷类型
- 重新生成 services/generated:ClientAuthError.ts 变成 `{ type } & 具名类型`,新增 19 个载荷类型文件
- tests/clientAuthError.test.ts:新增编译期用例,switch (error.type) 分支里错误窄化成具名载荷类型
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,14 @@
|
||||
//! 只在 Rust 内存与本进程会话快照里,refresh 凭据只写在 AppData 私有文件里。换号、登出或
|
||||
//! origin 变化都会让旧身份的在途请求失败关闭;同一身份的凭据轮换不改变身份代次。
|
||||
|
||||
use crate::auth_error::ClientAuthError;
|
||||
use crate::auth_error::{
|
||||
AuthClientInitFailed, AuthNetworkUnavailable, AuthResponseMalformed, AuthServiceUnavailable,
|
||||
ClientAuthError, ClientSessionPersistFailed, LoginCodeMissing, PasswordEntryInputRejected,
|
||||
PasswordMissing, PermissionDenied, PhoneLoginInputRejected, PhoneNumberInvalid,
|
||||
PhoneOrPasswordMismatch, RuntimeSessionInstallFailed, SendCodeInputRejected,
|
||||
ServerAddressRejected, SessionAuthorityRejected, SmsCodeInvalidOrExpired, SmsCodeThrottled,
|
||||
UnexpectedRejection,
|
||||
};
|
||||
use crate::http_client::agc_main_site_client_builder;
|
||||
use crate::platform_session::{current_platform_session, PlatformSessionSnapshot};
|
||||
use reqwest::{header::SET_COOKIE, Method, StatusCode};
|
||||
@@ -223,30 +230,32 @@ fn session_file_path(app: &tauri::AppHandle) -> Result<PathBuf, ClientAuthError>
|
||||
app.path()
|
||||
.app_data_dir()
|
||||
.map(|root| root.join(SESSION_FILE_NAME))
|
||||
.map_err(|error| ClientAuthError::ClientSessionPersistFailed {
|
||||
message: format!("无法读取 AGC 应用数据目录:{error}"),
|
||||
.map_err(|error| {
|
||||
ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed {
|
||||
message: format!("无法读取 AGC 应用数据目录:{error}"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// 凭据文件相关的字符串错误统一收口成凭据落盘失败变体。
|
||||
fn session_persist_error(message: impl Into<String>) -> ClientAuthError {
|
||||
ClientAuthError::ClientSessionPersistFailed {
|
||||
ClientAuthError::ClientSessionPersistFailed(ClientSessionPersistFailed {
|
||||
message: message.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 本机运行时安装 / 清理失败的字符串错误统一收口。
|
||||
fn runtime_session_error(message: impl Into<String>) -> ClientAuthError {
|
||||
ClientAuthError::RuntimeSessionInstallFailed {
|
||||
ClientAuthError::RuntimeSessionInstallFailed(RuntimeSessionInstallFailed {
|
||||
message: message.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 服务地址校验(含渠道范围门禁)失败统一收口成业务变体。
|
||||
fn server_address_rejected(message: impl Into<String>) -> ClientAuthError {
|
||||
ClientAuthError::ServerAddressRejected {
|
||||
ClientAuthError::ServerAddressRejected(ServerAddressRejected {
|
||||
message: message.into(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// 校验并归一化平台服务 origin。
|
||||
@@ -428,19 +437,21 @@ fn build_client() -> Result<reqwest::Client, ClientAuthError> {
|
||||
.connect_timeout(Duration::from_secs(10))
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.build()
|
||||
.map_err(|_| ClientAuthError::AuthClientInitFailed {
|
||||
message: "创建登录网络客户端失败".to_string(),
|
||||
.map_err(|_| {
|
||||
ClientAuthError::AuthClientInitFailed(AuthClientInitFailed {
|
||||
message: "创建登录网络客户端失败".to_string(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn network_error_message(error: &reqwest::Error) -> ClientAuthError {
|
||||
ClientAuthError::AuthNetworkUnavailable {
|
||||
ClientAuthError::AuthNetworkUnavailable(AuthNetworkUnavailable {
|
||||
message: if error.is_timeout() {
|
||||
AUTH_NETWORK_TIMEOUT.to_string()
|
||||
} else {
|
||||
AUTH_NETWORK_ERROR.to_string()
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn error_message(body: &str) -> Option<String> {
|
||||
@@ -508,79 +519,92 @@ fn map_auth_failure(
|
||||
};
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
return match route {
|
||||
AuthRoute::Session => ClientAuthError::SessionAuthorityRejected {
|
||||
message: server_message.unwrap_or_else(|| AUTH_AUTHORITY_MESSAGE.to_string()),
|
||||
},
|
||||
AuthRoute::PasswordEntry => ClientAuthError::PhoneOrPasswordMismatch {
|
||||
message: server_message.unwrap_or_else(|| fallback.to_string()),
|
||||
},
|
||||
AuthRoute::PhoneLogin => ClientAuthError::SmsCodeInvalidOrExpired {
|
||||
message: server_message.unwrap_or_else(|| fallback.to_string()),
|
||||
},
|
||||
_ => ClientAuthError::UnexpectedRejection {
|
||||
AuthRoute::Session => {
|
||||
ClientAuthError::SessionAuthorityRejected(SessionAuthorityRejected {
|
||||
message: server_message.unwrap_or_else(|| AUTH_AUTHORITY_MESSAGE.to_string()),
|
||||
})
|
||||
}
|
||||
AuthRoute::PasswordEntry => {
|
||||
ClientAuthError::PhoneOrPasswordMismatch(PhoneOrPasswordMismatch {
|
||||
message: server_message.unwrap_or_else(|| fallback.to_string()),
|
||||
})
|
||||
}
|
||||
AuthRoute::PhoneLogin => {
|
||||
ClientAuthError::SmsCodeInvalidOrExpired(SmsCodeInvalidOrExpired {
|
||||
message: server_message.unwrap_or_else(|| fallback.to_string()),
|
||||
})
|
||||
}
|
||||
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: status_code,
|
||||
message: server_message.unwrap_or_else(|| fallback.to_string()),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
if status == StatusCode::FORBIDDEN {
|
||||
return match route {
|
||||
AuthRoute::Session => ClientAuthError::PermissionDenied {
|
||||
AuthRoute::Session => ClientAuthError::PermissionDenied(PermissionDenied {
|
||||
message: server_message
|
||||
.unwrap_or_else(|| "当前陶泥儿账号没有执行此操作的权限".to_string()),
|
||||
},
|
||||
_ => ClientAuthError::UnexpectedRejection {
|
||||
}),
|
||||
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: status_code,
|
||||
message: server_message.unwrap_or_else(|| fallback.to_string()),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
if status == StatusCode::TOO_MANY_REQUESTS && route == AuthRoute::SendCode {
|
||||
return ClientAuthError::SmsCodeThrottled {
|
||||
return ClientAuthError::SmsCodeThrottled(SmsCodeThrottled {
|
||||
message: prefixed(),
|
||||
};
|
||||
});
|
||||
}
|
||||
if status == StatusCode::BAD_REQUEST {
|
||||
return match route {
|
||||
AuthRoute::PasswordEntry => ClientAuthError::PasswordEntryInputRejected {
|
||||
AuthRoute::PasswordEntry => {
|
||||
ClientAuthError::PasswordEntryInputRejected(PasswordEntryInputRejected {
|
||||
message: prefixed(),
|
||||
})
|
||||
}
|
||||
AuthRoute::PhoneLogin => {
|
||||
ClientAuthError::PhoneLoginInputRejected(PhoneLoginInputRejected {
|
||||
message: prefixed(),
|
||||
})
|
||||
}
|
||||
AuthRoute::SendCode => ClientAuthError::SendCodeInputRejected(SendCodeInputRejected {
|
||||
message: prefixed(),
|
||||
},
|
||||
AuthRoute::PhoneLogin => ClientAuthError::PhoneLoginInputRejected {
|
||||
message: prefixed(),
|
||||
},
|
||||
AuthRoute::SendCode => ClientAuthError::SendCodeInputRejected {
|
||||
message: prefixed(),
|
||||
},
|
||||
_ => ClientAuthError::UnexpectedRejection {
|
||||
}),
|
||||
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: status_code,
|
||||
message: prefixed(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
if status.is_server_error() {
|
||||
return ClientAuthError::AuthServiceUnavailable {
|
||||
return ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable {
|
||||
status: status_code,
|
||||
message: prefixed(),
|
||||
};
|
||||
});
|
||||
}
|
||||
ClientAuthError::UnexpectedRejection {
|
||||
ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: status_code,
|
||||
message: prefixed(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn response_data(body: &str, fallback: &str) -> Result<Value, ClientAuthError> {
|
||||
let value: Value =
|
||||
serde_json::from_str(body).map_err(|_| ClientAuthError::AuthResponseMalformed {
|
||||
let value: Value = serde_json::from_str(body).map_err(|_| {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: format!("{fallback}:登录服务响应不是合法 JSON"),
|
||||
})?;
|
||||
})
|
||||
})?;
|
||||
if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
||||
return Err(ClientAuthError::AuthResponseMalformed {
|
||||
message: format!(
|
||||
"{fallback}:{}",
|
||||
error_message(body).unwrap_or_else(|| "登录服务请求失败".to_string())
|
||||
),
|
||||
});
|
||||
return Err(ClientAuthError::AuthResponseMalformed(
|
||||
AuthResponseMalformed {
|
||||
message: format!(
|
||||
"{fallback}:{}",
|
||||
error_message(body).unwrap_or_else(|| "登录服务请求失败".to_string())
|
||||
),
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(value.get("data").cloned().unwrap_or(value))
|
||||
}
|
||||
@@ -657,19 +681,20 @@ async fn request_auth(
|
||||
CookiePolicy::Capture | CookiePolicy::Require => refresh_cookie_from_response(&response),
|
||||
CookiePolicy::Ignore => None,
|
||||
};
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|_| ClientAuthError::AuthResponseMalformed {
|
||||
let text = response.text().await.map_err(|_| {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: format!("{fallback}:读取响应失败"),
|
||||
})?;
|
||||
})
|
||||
})?;
|
||||
if !status.is_success() {
|
||||
return Err(map_auth_failure(status, &text, fallback, auth_route(route)));
|
||||
}
|
||||
if matches!(policy, CookiePolicy::Require) && captured.is_none() {
|
||||
return Err(ClientAuthError::AuthResponseMalformed {
|
||||
message: "登录服务未返回新的续期凭据,已停止使用旧凭据".to_string(),
|
||||
});
|
||||
return Err(ClientAuthError::AuthResponseMalformed(
|
||||
AuthResponseMalformed {
|
||||
message: "登录服务未返回新的续期凭据,已停止使用旧凭据".to_string(),
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(AuthResponse {
|
||||
data: response_data(&text, fallback)?,
|
||||
@@ -708,9 +733,11 @@ async fn commit_authenticated_session(
|
||||
identity_change: bool,
|
||||
) -> Result<AuthUserPayload, ClientAuthError> {
|
||||
if token.chars().count() > MAX_SECRET_CHARS {
|
||||
return Err(ClientAuthError::AuthResponseMalformed {
|
||||
message: "登录服务返回的凭据无效".to_string(),
|
||||
});
|
||||
return Err(ClientAuthError::AuthResponseMalformed(
|
||||
AuthResponseMalformed {
|
||||
message: "登录服务返回的凭据无效".to_string(),
|
||||
},
|
||||
));
|
||||
}
|
||||
// 主体先于凭据落盘解析:没有主体就不写 client-session.json,避免留下半截会话文件。
|
||||
let (user_id, known_user) = identity.resolve()?;
|
||||
@@ -802,9 +829,11 @@ fn phone_is_valid(phone: &str) -> bool {
|
||||
fn validated_session_user_id(user_id: &str) -> Result<String, ClientAuthError> {
|
||||
let user_id = user_id.trim();
|
||||
if user_id.is_empty() {
|
||||
return Err(ClientAuthError::AuthResponseMalformed {
|
||||
message: "登录失败:登录服务未返回用户身份".to_string(),
|
||||
});
|
||||
return Err(ClientAuthError::AuthResponseMalformed(
|
||||
AuthResponseMalformed {
|
||||
message: "登录失败:登录服务未返回用户身份".to_string(),
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(user_id.to_string())
|
||||
}
|
||||
@@ -837,9 +866,9 @@ async fn fetch_current_user(
|
||||
)
|
||||
.await?;
|
||||
let me: MeResponse = serde_json::from_value(response.data).map_err(|_| {
|
||||
ClientAuthError::AuthResponseMalformed {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: "读取当前用户失败:响应格式无效".to_string(),
|
||||
}
|
||||
})
|
||||
})?;
|
||||
Ok(me.user)
|
||||
}
|
||||
@@ -917,17 +946,16 @@ async fn refresh_session_inner(
|
||||
}
|
||||
};
|
||||
let token: TokenResponse = serde_json::from_value(refreshed.data).map_err(|_| {
|
||||
ClientAuthError::AuthResponseMalformed {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: "刷新登录状态失败:凭据响应格式无效".to_string(),
|
||||
}
|
||||
})
|
||||
})?;
|
||||
let install_token = token.token.clone();
|
||||
let new_cookie =
|
||||
refreshed
|
||||
.refresh_cookie
|
||||
.ok_or_else(|| ClientAuthError::AuthResponseMalformed {
|
||||
message: "刷新登录状态失败:缺少新的续期凭据".to_string(),
|
||||
})?;
|
||||
let new_cookie = refreshed.refresh_cookie.ok_or_else(|| {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: "刷新登录状态失败:缺少新的续期凭据".to_string(),
|
||||
})
|
||||
})?;
|
||||
commit_authenticated_session(
|
||||
app,
|
||||
&session.api_base_url,
|
||||
@@ -1073,9 +1101,9 @@ pub(crate) async fn send_client_phone_login_code(
|
||||
let origin = validate_client_api_base_url(&api_base_url).map_err(server_address_rejected)?;
|
||||
let phone = phone.trim();
|
||||
if !phone_is_valid(phone) {
|
||||
return Err(ClientAuthError::PhoneNumberInvalid {
|
||||
return Err(ClientAuthError::PhoneNumberInvalid(PhoneNumberInvalid {
|
||||
message: "请输入正确的手机号".to_string(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
let client = build_client()?;
|
||||
let response = request_auth(
|
||||
@@ -1094,9 +1122,9 @@ pub(crate) async fn send_client_phone_login_code(
|
||||
)
|
||||
.await?;
|
||||
let payload: SendCodeResponse = serde_json::from_value(response.data).map_err(|_| {
|
||||
ClientAuthError::AuthResponseMalformed {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: "发送验证码失败:响应格式无效".to_string(),
|
||||
}
|
||||
})
|
||||
})?;
|
||||
Ok(ClientLoginCodeView {
|
||||
cooldown_seconds: payload.cooldown_seconds,
|
||||
@@ -1110,15 +1138,15 @@ async fn complete_login(
|
||||
response: AuthResponse,
|
||||
) -> Result<AuthUserPayload, ClientAuthError> {
|
||||
let payload: TokenUserResponse = serde_json::from_value(response.data).map_err(|_| {
|
||||
ClientAuthError::AuthResponseMalformed {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: "登录失败:登录服务响应格式无效".to_string(),
|
||||
}
|
||||
})
|
||||
})?;
|
||||
let cookie = response
|
||||
.refresh_cookie
|
||||
.ok_or_else(|| ClientAuthError::AuthResponseMalformed {
|
||||
let cookie = response.refresh_cookie.ok_or_else(|| {
|
||||
ClientAuthError::AuthResponseMalformed(AuthResponseMalformed {
|
||||
message: "登录失败:登录服务未返回续期凭据".to_string(),
|
||||
})?;
|
||||
})
|
||||
})?;
|
||||
let user = commit_authenticated_session(
|
||||
app,
|
||||
origin,
|
||||
@@ -1148,14 +1176,14 @@ pub(crate) async fn login_client_with_password(
|
||||
let origin = validate_client_api_base_url(&api_base_url).map_err(server_address_rejected)?;
|
||||
let phone = phone.trim();
|
||||
if !phone_is_valid(phone) {
|
||||
return Err(ClientAuthError::PhoneNumberInvalid {
|
||||
return Err(ClientAuthError::PhoneNumberInvalid(PhoneNumberInvalid {
|
||||
message: "请输入正确的手机号".to_string(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
if password.trim().is_empty() {
|
||||
return Err(ClientAuthError::PasswordMissing {
|
||||
return Err(ClientAuthError::PasswordMissing(PasswordMissing {
|
||||
message: "请输入密码".to_string(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
let client = build_client()?;
|
||||
let response = request_auth(
|
||||
@@ -1186,14 +1214,14 @@ pub(crate) async fn login_client_with_phone_code(
|
||||
let origin = validate_client_api_base_url(&api_base_url).map_err(server_address_rejected)?;
|
||||
let phone = phone.trim();
|
||||
if !phone_is_valid(phone) {
|
||||
return Err(ClientAuthError::PhoneNumberInvalid {
|
||||
return Err(ClientAuthError::PhoneNumberInvalid(PhoneNumberInvalid {
|
||||
message: "请输入正确的手机号".to_string(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
if code.trim().is_empty() {
|
||||
return Err(ClientAuthError::LoginCodeMissing {
|
||||
return Err(ClientAuthError::LoginCodeMissing(LoginCodeMissing {
|
||||
message: "请输入验证码".to_string(),
|
||||
});
|
||||
}));
|
||||
}
|
||||
let client = build_client()?;
|
||||
let response = request_auth(
|
||||
@@ -1321,7 +1349,7 @@ mod tests {
|
||||
assert!(!transient.is_authority_failure());
|
||||
assert!(matches!(
|
||||
transient,
|
||||
ClientAuthError::AuthServiceUnavailable { status: 500, .. }
|
||||
ClientAuthError::AuthServiceUnavailable(AuthServiceUnavailable { status: 500, .. })
|
||||
));
|
||||
assert!(transient.message().starts_with("刷新失败"));
|
||||
}
|
||||
@@ -1336,9 +1364,9 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
wrong_password,
|
||||
ClientAuthError::PhoneOrPasswordMismatch {
|
||||
ClientAuthError::PhoneOrPasswordMismatch(PhoneOrPasswordMismatch {
|
||||
message: "手机号或密码错误".to_string()
|
||||
}
|
||||
})
|
||||
);
|
||||
assert!(!wrong_password.is_authority_failure());
|
||||
|
||||
@@ -1361,7 +1389,7 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(
|
||||
password_length,
|
||||
ClientAuthError::PasswordEntryInputRejected { .. }
|
||||
ClientAuthError::PasswordEntryInputRejected(PasswordEntryInputRejected { .. })
|
||||
));
|
||||
assert_eq!(
|
||||
password_length.message(),
|
||||
@@ -1376,7 +1404,7 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(
|
||||
throttled,
|
||||
ClientAuthError::SmsCodeThrottled { .. }
|
||||
ClientAuthError::SmsCodeThrottled(SmsCodeThrottled { .. })
|
||||
));
|
||||
assert_eq!(throttled.message(), "发送验证码失败:发送过于频繁");
|
||||
|
||||
@@ -1388,7 +1416,7 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(
|
||||
bad_code,
|
||||
ClientAuthError::SmsCodeInvalidOrExpired { .. }
|
||||
ClientAuthError::SmsCodeInvalidOrExpired(SmsCodeInvalidOrExpired { .. })
|
||||
));
|
||||
assert_eq!(bad_code.message(), "验证码错误");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 认证网络客户端构建失败。
|
||||
*/
|
||||
export type AuthClientInitFailed = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 连接 / 超时 / DNS 等网络失败。
|
||||
*/
|
||||
export type AuthNetworkUnavailable = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 响应不是合法 JSON、缺少必需字段或契约不成立。
|
||||
*/
|
||||
export type AuthResponseMalformed = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 服务端 5xx。
|
||||
*/
|
||||
export type AuthServiceUnavailable = { status: number; message: string };
|
||||
@@ -1,25 +1,44 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AuthClientInitFailed } from './AuthClientInitFailed';
|
||||
import type { AuthNetworkUnavailable } from './AuthNetworkUnavailable';
|
||||
import type { AuthResponseMalformed } from './AuthResponseMalformed';
|
||||
import type { AuthServiceUnavailable } from './AuthServiceUnavailable';
|
||||
import type { ClientSessionPersistFailed } from './ClientSessionPersistFailed';
|
||||
import type { LoginCodeMissing } from './LoginCodeMissing';
|
||||
import type { PasswordEntryInputRejected } from './PasswordEntryInputRejected';
|
||||
import type { PasswordMissing } from './PasswordMissing';
|
||||
import type { PermissionDenied } from './PermissionDenied';
|
||||
import type { PhoneLoginInputRejected } from './PhoneLoginInputRejected';
|
||||
import type { PhoneNumberInvalid } from './PhoneNumberInvalid';
|
||||
import type { PhoneOrPasswordMismatch } from './PhoneOrPasswordMismatch';
|
||||
import type { RuntimeSessionInstallFailed } from './RuntimeSessionInstallFailed';
|
||||
import type { SendCodeInputRejected } from './SendCodeInputRejected';
|
||||
import type { ServerAddressRejected } from './ServerAddressRejected';
|
||||
import type { SessionAuthorityRejected } from './SessionAuthorityRejected';
|
||||
import type { SmsCodeInvalidOrExpired } from './SmsCodeInvalidOrExpired';
|
||||
import type { SmsCodeThrottled } from './SmsCodeThrottled';
|
||||
import type { UnexpectedRejection } from './UnexpectedRejection';
|
||||
|
||||
/**
|
||||
* 变体名就是线上的分流键(`type`)。
|
||||
* 变体名就是线上的分流键(`type`),每个变体持有同名载荷类型。
|
||||
*/
|
||||
export type ClientAuthError =
|
||||
| { type: 'serverAddressRejected'; message: string }
|
||||
| { type: 'phoneNumberInvalid'; message: string }
|
||||
| { type: 'passwordMissing'; message: string }
|
||||
| { type: 'loginCodeMissing'; message: string }
|
||||
| { type: 'passwordEntryInputRejected'; message: string }
|
||||
| { type: 'phoneOrPasswordMismatch'; message: string }
|
||||
| { type: 'sendCodeInputRejected'; message: string }
|
||||
| { type: 'smsCodeThrottled'; message: string }
|
||||
| { type: 'phoneLoginInputRejected'; message: string }
|
||||
| { type: 'smsCodeInvalidOrExpired'; message: string }
|
||||
| { type: 'sessionAuthorityRejected'; message: string }
|
||||
| { type: 'permissionDenied'; message: string }
|
||||
| { type: 'authNetworkUnavailable'; message: string }
|
||||
| { type: 'authServiceUnavailable'; status: number; message: string }
|
||||
| { type: 'unexpectedRejection'; status: number; message: string }
|
||||
| { type: 'authResponseMalformed'; message: string }
|
||||
| { type: 'clientSessionPersistFailed'; message: string }
|
||||
| { type: 'runtimeSessionInstallFailed'; message: string }
|
||||
| { type: 'authClientInitFailed'; message: string };
|
||||
| ({ type: 'serverAddressRejected' } & ServerAddressRejected)
|
||||
| ({ type: 'phoneNumberInvalid' } & PhoneNumberInvalid)
|
||||
| ({ type: 'passwordMissing' } & PasswordMissing)
|
||||
| ({ type: 'loginCodeMissing' } & LoginCodeMissing)
|
||||
| ({ type: 'passwordEntryInputRejected' } & PasswordEntryInputRejected)
|
||||
| ({ type: 'phoneOrPasswordMismatch' } & PhoneOrPasswordMismatch)
|
||||
| ({ type: 'sendCodeInputRejected' } & SendCodeInputRejected)
|
||||
| ({ type: 'smsCodeThrottled' } & SmsCodeThrottled)
|
||||
| ({ type: 'phoneLoginInputRejected' } & PhoneLoginInputRejected)
|
||||
| ({ type: 'smsCodeInvalidOrExpired' } & SmsCodeInvalidOrExpired)
|
||||
| ({ type: 'sessionAuthorityRejected' } & SessionAuthorityRejected)
|
||||
| ({ type: 'permissionDenied' } & PermissionDenied)
|
||||
| ({ type: 'authNetworkUnavailable' } & AuthNetworkUnavailable)
|
||||
| ({ type: 'authServiceUnavailable' } & AuthServiceUnavailable)
|
||||
| ({ type: 'unexpectedRejection' } & UnexpectedRejection)
|
||||
| ({ type: 'authResponseMalformed' } & AuthResponseMalformed)
|
||||
| ({ type: 'clientSessionPersistFailed' } & ClientSessionPersistFailed)
|
||||
| ({ type: 'runtimeSessionInstallFailed' } & RuntimeSessionInstallFailed)
|
||||
| ({ type: 'authClientInitFailed' } & AuthClientInitFailed);
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 本机登录凭据文件读写失败。
|
||||
*/
|
||||
export type ClientSessionPersistFailed = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 本地前置校验:验证码为空。
|
||||
*/
|
||||
export type LoginCodeMissing = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/entry` 返回 400:服务端拒绝本次输入。
|
||||
*/
|
||||
export type PasswordEntryInputRejected = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 本地前置校验:密码为空。
|
||||
*/
|
||||
export type PasswordMissing = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 会话路由 403:当前账号没有执行此操作的权限。
|
||||
*/
|
||||
export type PermissionDenied = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/phone/login` 返回 400。
|
||||
*/
|
||||
export type PhoneLoginInputRejected = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 本地前置校验:手机号为空或格式不合法。
|
||||
*/
|
||||
export type PhoneNumberInvalid = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/entry` 返回 401:手机号或密码错误。
|
||||
*/
|
||||
export type PhoneOrPasswordMismatch = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 本机运行时会话安装 / 清理失败。
|
||||
*/
|
||||
export type RuntimeSessionInstallFailed = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/phone/send-code` 返回 400。
|
||||
*/
|
||||
export type SendCodeInputRejected = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 服务地址不是合法 origin / 非本机未用 HTTPS / 不在构建渠道范围内。
|
||||
*/
|
||||
export type ServerAddressRejected = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 会话路由 401:登录态权威失效。
|
||||
*/
|
||||
export type SessionAuthorityRejected = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/phone/login` 返回 401:验证码错误或过期。
|
||||
*/
|
||||
export type SmsCodeInvalidOrExpired = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `/api/auth/phone/send-code` 返回 429:发送过于频繁。
|
||||
*/
|
||||
export type SmsCodeThrottled = { message: string };
|
||||
@@ -0,0 +1,6 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 其它未识别的拒绝(未列举的 4xx、登录路由 403 等)。
|
||||
*/
|
||||
export type UnexpectedRejection = { status: number; message: string };
|
||||
@@ -1,8 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { isClientAuthError } from '../src/services/clientAuthError';
|
||||
import type { AuthServiceUnavailable } from '../src/services/generated/AuthServiceUnavailable';
|
||||
import type { ClientAuthError } from '../src/services/generated/ClientAuthError';
|
||||
import type { PhoneNumberInvalid } from '../src/services/generated/PhoneNumberInvalid';
|
||||
|
||||
/**
|
||||
* 编译期用例:每个变体在 `type` 上判别后都能落到一个**具名载荷类型**,
|
||||
* 等价于 Java 的 `catch (PhoneNumberInvalid e)`;类型不匹配时 `tsc` 会直接报错。
|
||||
*/
|
||||
function handleByNamedType(error: ClientAuthError) {
|
||||
switch (error.type) {
|
||||
case 'phoneNumberInvalid': {
|
||||
const payload: PhoneNumberInvalid = error;
|
||||
return payload.message;
|
||||
}
|
||||
case 'authServiceUnavailable': {
|
||||
const payload: AuthServiceUnavailable = error;
|
||||
return `${payload.status}:${payload.message}`;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
describe('AGC 认证命令错误的形状读取', () => {
|
||||
it('判别后按具名载荷类型分支处理', () => {
|
||||
expect(
|
||||
handleByNamedType({
|
||||
type: 'phoneNumberInvalid',
|
||||
message: '请输入正确的手机号',
|
||||
}),
|
||||
).toBe('请输入正确的手机号');
|
||||
expect(
|
||||
handleByNamedType({
|
||||
type: 'authServiceUnavailable',
|
||||
status: 503,
|
||||
message: '登录服务暂时不可用',
|
||||
}),
|
||||
).toBe('503:登录服务暂时不可用');
|
||||
});
|
||||
|
||||
it('只按形状读取 Rust 的结构化拒绝,裸字符串与 Error 都不算', () => {
|
||||
expect(
|
||||
isClientAuthError({
|
||||
|
||||
Reference in New Issue
Block a user