手机号登录401归phoneCodeLoginRejected
- auth_session:/api/auth/phone/login 的 401(只来自「用户不存在」)改判为 phoneCodeLoginRejected{serverMessage},不再冒充「验证码错误或过期」
- auth_error:退役顶层变体 smsCodeRejected(唯一产生点就是这个错配的 401 分支)
- 前端三个 catch 删掉 smsCodeRejected 分支;phoneCodeLoginRejected 文案统一为「验证码登录失败:<服务端原文>」,冒号改全角
- ts-rs 重新生成 ClientAuthError 联合;carrier 用例同步删掉 smsCodeRejected
- 文档:ADR + decision-log 记录 401 归属与 smsCodeRejected 退役
This commit is contained in:
@@ -37,10 +37,10 @@ pub(crate) enum ClientAuthError {
|
||||
SendCodeRejected(SendCodeRejected),
|
||||
/// `/api/auth/phone/send-code` 返回 429:发送过于频繁。
|
||||
SmsCodeThrottled,
|
||||
/// `/api/auth/phone/login` 返回 400:服务端拒绝本次输入。
|
||||
/// `/api/auth/phone/login` 返回 400/401:服务端拒绝本次验证码登录。
|
||||
///
|
||||
/// 401 目前只来自「用户不存在」;该路由验证通过后会即时建号,所以这条分支实际很少触发。
|
||||
PhoneCodeLoginRejected(PhoneCodeLoginRejected),
|
||||
/// `/api/auth/phone/login` 返回 401:验证码错误或过期。
|
||||
SmsCodeRejected,
|
||||
|
||||
// ---- 会话:调用方按"未登录"处理,不给用户报错 ----
|
||||
/// 会话路由 401:登录态失效。
|
||||
@@ -259,7 +259,6 @@ mod tests {
|
||||
}),
|
||||
"phoneCodeLoginRejected",
|
||||
),
|
||||
(ClientAuthError::SmsCodeRejected, "smsCodeRejected"),
|
||||
(ClientAuthError::SessionInvalidated, "sessionInvalidated"),
|
||||
(ClientAuthError::PermissionDenied, "permissionDenied"),
|
||||
(
|
||||
|
||||
@@ -515,7 +515,9 @@ fn map_auth_failure(status: StatusCode, body: &str, route: AuthRoute) -> ClientA
|
||||
return match route {
|
||||
AuthRoute::Session => ClientAuthError::SessionInvalidated,
|
||||
AuthRoute::PasswordEntry => ClientAuthError::PhoneOrPasswordMismatch,
|
||||
AuthRoute::PhoneLogin => ClientAuthError::SmsCodeRejected,
|
||||
AuthRoute::PhoneLogin => {
|
||||
ClientAuthError::PhoneCodeLoginRejected(PhoneCodeLoginRejected { server_message })
|
||||
}
|
||||
_ => ClientAuthError::UnexpectedRejection(UnexpectedRejection {
|
||||
status: status_code,
|
||||
server_message,
|
||||
@@ -1391,12 +1393,18 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
let bad_code = map_auth_failure(
|
||||
// 登录路由的 401 只来自「用户不存在」:仍是业务变体,保留服务端原文,不能进系统变体。
|
||||
let user_not_found = map_auth_failure(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
r#"{"error":{"message":"验证码错误"}}"#,
|
||||
r#"{"error":{"message":"用户不存在"}}"#,
|
||||
AuthRoute::PhoneLogin,
|
||||
);
|
||||
assert_eq!(bad_code, ClientAuthError::SmsCodeRejected);
|
||||
assert_eq!(
|
||||
user_not_found,
|
||||
ClientAuthError::PhoneCodeLoginRejected(PhoneCodeLoginRejected {
|
||||
server_message: Some("用户不存在".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -363,13 +363,7 @@ export function AuthenticatedClient({
|
||||
}
|
||||
case 'phoneCodeLoginRejected': {
|
||||
const payload = failure as PhoneCodeLoginRejected;
|
||||
const text = `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeRejected': {
|
||||
const text = '验证码错误或已过期';
|
||||
const text = `验证码登录失败:${payload.serverMessage ?? '服务端拒绝了本次请求'}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
@@ -632,14 +626,10 @@ export function AuthenticatedClient({
|
||||
case 'phoneCodeLoginRejected': {
|
||||
const payload = failure as PhoneCodeLoginRejected;
|
||||
setLoginStatus(
|
||||
`验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
`验证码登录失败:${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeRejected': {
|
||||
setLoginStatus('验证码错误或已过期');
|
||||
break;
|
||||
}
|
||||
case 'sessionInvalidated': {
|
||||
setLoginStatus('登录状态已失效,请重新登录');
|
||||
break;
|
||||
@@ -863,14 +853,10 @@ export function AuthenticatedClient({
|
||||
case 'phoneCodeLoginRejected': {
|
||||
const payload = failure as PhoneCodeLoginRejected;
|
||||
setLoginStatus(
|
||||
`验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
`验证码登录失败:${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeRejected': {
|
||||
setLoginStatus('验证码错误或已过期');
|
||||
break;
|
||||
}
|
||||
case 'sessionInvalidated': {
|
||||
setLoginStatus('登录状态已失效,请重新登录');
|
||||
break;
|
||||
|
||||
@@ -24,7 +24,6 @@ export type ClientAuthError =
|
||||
| ({ type: 'sendCodeRejected' } & SendCodeRejected)
|
||||
| { type: 'smsCodeThrottled' }
|
||||
| ({ type: 'phoneCodeLoginRejected' } & PhoneCodeLoginRejected)
|
||||
| { type: 'smsCodeRejected' }
|
||||
| { type: 'sessionInvalidated' }
|
||||
| { type: 'permissionDenied' }
|
||||
| ({ type: 'authNetworkFailure' } & AuthNetworkFailure)
|
||||
|
||||
@@ -73,8 +73,6 @@ function variantMessage(error: ClientAuthError): string {
|
||||
return (
|
||||
(error as PhoneCodeLoginRejected).serverMessage ?? '验证码登录被拒绝'
|
||||
);
|
||||
case 'smsCodeRejected':
|
||||
return '验证码错误或过期';
|
||||
case 'sessionInvalidated':
|
||||
return '登录态失效';
|
||||
case 'permissionDenied':
|
||||
|
||||
@@ -59,6 +59,8 @@ DirectProject 已经解决过同一类问题([`【ADR】DirectProject命令接
|
||||
- `429` 同样按路由判定:发码路由是频控(`smsCodeThrottled`),登录路由是「验证码错误次数过多」
|
||||
(复用 `phoneCodeLoginRejected`,仍是用户可修正的输入问题、不进池),其余路由的 `429` 才落到
|
||||
`unexpectedRejection`。
|
||||
- `/api/auth/phone/login` 的 `401` 只来自「用户不存在」(验证码错误/失效/过期都是 `400`),同样归
|
||||
`phoneCodeLoginRejected`;为此退役的 `smsCodeRejected` 曾把 401 冒充成「验证码错误或过期」,属于错配。
|
||||
- 本地前置校验只做布尔判定、拿不到更细事实时不编字段:手机号校验 `phone_is_valid` 只回 true/false,
|
||||
所以 `phoneNumberInvalid` 保持无字段,提示由调用方给固定文案;编不出事实的"细分原因"不硬造。
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
- 决策(2026-10-01,判定位置与出口):要不要上报只由 catch 子句里的 `switch (failure.type)` 判,`failure = error.error`;无字段业务 / 会话变体用本 catch 的固定文案,带载荷变体先 `as` 取自己的具名类型、再用它自己的 `reason` / `serverMessage` / `status` 拼上本次操作的上下文前缀(`reason` 是枚举时再 `switch (payload.reason)`);Rust 不预拼用户可见文案、服务端原文缺失就是 `null`(无兜底文案)。系统变体原样 `throw` 经全局 `unhandledrejection` 入池(`captureClientError` 用 `instanceof` 解包 `error` 字段取原始错误),`default: expectNever(failure)` 让漏接变体编译失败。取代"未识别变体上调是故意的"。
|
||||
- 决策(2026-10-01,Rust 侧不再降级):`refresh_session_inner` 的非权威失败直接 `Err(ClientAuthError)`,`ClientAuthStateView` / `ClientAuthRefreshView` 删除 `errorMessage`,续期结果删除 `failed`;`ClientAuthState` 收敛为 `authenticated | unauthenticated`,`ClientAuthRefreshResult` 收敛为 `refreshed | unauthenticated | stale`。
|
||||
- 追加(2026-10-02,429 按路由判定):`/api/auth/phone/login` 验证码错误次数耗尽返回的 429 是用户可修正的输入问题,映射为 `phoneCodeLoginRejected`(复用现有业务变体、不进错误池);发码路由仍是 `smsCodeThrottled`,其余路由的 429 仍是 `unexpectedRejection`。
|
||||
- 追加(2026-10-02,401 归 phoneCodeLoginRejected):`/api/auth/phone/login` 的 401 只来自「用户不存在」(验证码错误/失效/过期在服务端都是 400,已由 `phoneCodeLoginRejected { serverMessage }` 带原文);顶层变体 `smsCodeRejected` 退役删除,前端三个 catch 去掉了它那个「验证码错误或已过期」的固定分支,`phoneCodeLoginRejected` 的文案统一为「验证码登录失败:<服务端原文>」。
|
||||
- 追加(2026-10-02,系统类失败保留原始错误载荷):`clientSessionPersistFailed` / `runtimeSessionInstallFailed` / `authClientInitFailed` 都带 `detail: string`(原始 error),既让调用方有机会分流处理,也让报告包带够诊断信息;原始 error 同时经 `app_log!`(落盘前过 `sanitize_diagnostic_message`)记一行本地日志。`detail` 不贴到界面上:三个 catch 用本操作的固定文案(登录检查 / 发码 / 登录各自不同)。取代上一版"本机 IO 失败不进载荷、原始 error 只进日志"。
|
||||
- 影响范围(2026-10-01 第二轮):`apps/ai-game-creator-shell/src/services/{clientAuth.ts,platformSession.ts}`(`clientAuthError.ts` 删除)、`apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx`、`apps/ai-game-creator-shell/src-tauri/src/auth_session.rs`、对应 vitest 用例。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user