AGC认证失败按新变体分流并补上下文
- AuthenticatedClient 的三个 catch 列全新变体:无字段变体直接给固定文案,带载荷变体先 as 再读 serverMessage - 系统变体逐个列出后原样抛出,default 仍用 expectNever 把漏接变体卡在编译期 - clientAuth/ClientAuthErrorWrapper 注释去掉对 message 字段的假设
This commit is contained in:
@@ -35,18 +35,9 @@ import {
|
||||
captureClientError,
|
||||
installWebviewLogBridge,
|
||||
} from '../services/errorReporting';
|
||||
import type { LoginCodeMissing } from '../services/generated/LoginCodeMissing';
|
||||
import type { PasswordEntryInputRejected } from '../services/generated/PasswordEntryInputRejected';
|
||||
import type { PasswordMissing } from '../services/generated/PasswordMissing';
|
||||
import type { PermissionDenied } from '../services/generated/PermissionDenied';
|
||||
import type { PhoneLoginInputRejected } from '../services/generated/PhoneLoginInputRejected';
|
||||
import type { PhoneNumberInvalid } from '../services/generated/PhoneNumberInvalid';
|
||||
import type { PhoneOrPasswordMismatch } from '../services/generated/PhoneOrPasswordMismatch';
|
||||
import type { SendCodeInputRejected } from '../services/generated/SendCodeInputRejected';
|
||||
import type { ServerAddressRejected } from '../services/generated/ServerAddressRejected';
|
||||
import type { SessionAuthorityRejected } from '../services/generated/SessionAuthorityRejected';
|
||||
import type { SmsCodeInvalidOrExpired } from '../services/generated/SmsCodeInvalidOrExpired';
|
||||
import type { SmsCodeThrottled } from '../services/generated/SmsCodeThrottled';
|
||||
import type { PasswordLoginRejected } from '../services/generated/PasswordLoginRejected';
|
||||
import type { PhoneCodeLoginRejected } from '../services/generated/PhoneCodeLoginRejected';
|
||||
import type { SendCodeRejected } from '../services/generated/SendCodeRejected';
|
||||
import {
|
||||
beginPlatformSessionClearTransition,
|
||||
beginPlatformSessionTransition,
|
||||
@@ -280,76 +271,120 @@ export function AuthenticatedClient({
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error;
|
||||
const failure = error.error;
|
||||
switch (failure.type) {
|
||||
// 用户自己能改的输入 / 前置条件:展示原因并停在登录页,不进错误池。
|
||||
case 'serverAddressRejected': {
|
||||
const payload = failure as ServerAddressRejected;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
// 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。
|
||||
case 'serverAddressEmptyOrTooLong': {
|
||||
const text = '服务器地址非法: 不能为空或超长';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'serverAddressNotAUrl': {
|
||||
const text = '服务器地址非法: 不是合法地址';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'serverAddressHasCredentials': {
|
||||
const text = '服务器地址非法: 不能包含用户名或密码';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'serverAddressHasPathOrQueryOrFragment': {
|
||||
const text = '服务器地址非法: 不能带路径、查询或片段';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'serverAddressNotHttps': {
|
||||
const text = '服务器地址非法: 远程地址必须使用 https';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'serverAddressUnsupportedScheme': {
|
||||
const text = '服务器地址非法: 只支持 http 或 https';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'serverAddressOutsideChannel': {
|
||||
const text = '服务器地址非法: 不在当前构建渠道范围内';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'phoneNumberInvalid': {
|
||||
const payload = failure as PhoneNumberInvalid;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
const text = '手机号无效: 需为纯数字且不超过 32 位';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'passwordMissing': {
|
||||
const payload = failure as PasswordMissing;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
const text = '密码无效: 不能为空';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'loginCodeMissing': {
|
||||
const payload = failure as LoginCodeMissing;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
const text = '验证码无效: 不能为空';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'passwordEntryInputRejected': {
|
||||
const payload = failure as PasswordEntryInputRejected;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
case 'passwordLoginRejected': {
|
||||
const payload = failure as PasswordLoginRejected;
|
||||
const text = `登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'phoneOrPasswordMismatch': {
|
||||
const payload = failure as PhoneOrPasswordMismatch;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
const text = '手机号或密码错误';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'sendCodeInputRejected': {
|
||||
const payload = failure as SendCodeInputRejected;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
case 'sendCodeRejected': {
|
||||
const payload = failure as SendCodeRejected;
|
||||
const text = `发送验证码失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeThrottled': {
|
||||
const payload = failure as SmsCodeThrottled;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
const text = '发送验证码过于频繁,请稍后再试';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'phoneLoginInputRejected': {
|
||||
const payload = failure as PhoneLoginInputRejected;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
case 'phoneCodeLoginRejected': {
|
||||
const payload = failure as PhoneCodeLoginRejected;
|
||||
const text = `验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`;
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeInvalidOrExpired': {
|
||||
const payload = failure as SmsCodeInvalidOrExpired;
|
||||
setAuthCheckError(payload.message);
|
||||
setLoginStatus(payload.message);
|
||||
case 'smsCodeRejected': {
|
||||
const text = '验证码错误或已过期';
|
||||
setAuthCheckError(text);
|
||||
setLoginStatus(text);
|
||||
break;
|
||||
}
|
||||
// 会话权威失效:Rust 已按未登录处理;走到这里也只按未登录呈现。
|
||||
case 'sessionAuthorityRejected':
|
||||
case 'sessionInvalidated':
|
||||
case 'permissionDenied':
|
||||
break;
|
||||
// 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkUnavailable':
|
||||
case 'authNetworkTimeout':
|
||||
case 'authNetworkUnreachable':
|
||||
case 'authServiceUnavailable':
|
||||
case 'unexpectedRejection':
|
||||
case 'authResponseMalformed':
|
||||
case 'authResponseNotJson':
|
||||
case 'authResponseInvalidBody':
|
||||
case 'authResponseMissingRefreshCookie':
|
||||
case 'authResponseMissingUserIdentity':
|
||||
case 'authResponseServerRejected':
|
||||
case 'clientSessionPersistFailed':
|
||||
case 'runtimeSessionInstallFailed':
|
||||
case 'authClientInitFailed':
|
||||
@@ -421,7 +456,8 @@ export function AuthenticatedClient({
|
||||
*
|
||||
* - 承载:命令失败由 `invokeClientAuth` 装进 `ClientAuthErrorWrapper`,`error` 就是判别联合,
|
||||
* `failure.type` 是唯一分流键;
|
||||
* - 业务 / 会话变体:把载荷自带的 `message` 原样给用户,永不进错误池;
|
||||
* - 业务 / 会话变体:无载荷变体用本 catch 的固定文案;带载荷变体先 `as` 取具名类型,
|
||||
* 再用它自己的 `serverMessage` 拼上下文(Rust 不预拼用户可见文案),永不进错误池;
|
||||
* - 系统变体:原样 `throw`,经全局 `unhandledrejection` 交给错误池;
|
||||
* - `default: expectNever` 保证 Rust 新增变体时这里编译失败。
|
||||
*
|
||||
@@ -452,71 +488,98 @@ export function AuthenticatedClient({
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error;
|
||||
const failure = error.error;
|
||||
switch (failure.type) {
|
||||
case 'serverAddressRejected': {
|
||||
const payload = failure as ServerAddressRejected;
|
||||
setLoginStatus(payload.message);
|
||||
// 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。
|
||||
case 'serverAddressEmptyOrTooLong': {
|
||||
setLoginStatus('服务器地址非法: 不能为空或超长');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressNotAUrl': {
|
||||
setLoginStatus('服务器地址非法: 不是合法地址');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressHasCredentials': {
|
||||
setLoginStatus('服务器地址非法: 不能包含用户名或密码');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressHasPathOrQueryOrFragment': {
|
||||
setLoginStatus('服务器地址非法: 不能带路径、查询或片段');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressNotHttps': {
|
||||
setLoginStatus('服务器地址非法: 远程地址必须使用 https');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressUnsupportedScheme': {
|
||||
setLoginStatus('服务器地址非法: 只支持 http 或 https');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressOutsideChannel': {
|
||||
setLoginStatus('服务器地址非法: 不在当前构建渠道范围内');
|
||||
break;
|
||||
}
|
||||
case 'phoneNumberInvalid': {
|
||||
const payload = failure as PhoneNumberInvalid;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('手机号无效: 需为纯数字且不超过 32 位');
|
||||
break;
|
||||
}
|
||||
case 'passwordMissing': {
|
||||
const payload = failure as PasswordMissing;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('密码无效: 不能为空');
|
||||
break;
|
||||
}
|
||||
case 'loginCodeMissing': {
|
||||
const payload = failure as LoginCodeMissing;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('验证码无效: 不能为空');
|
||||
break;
|
||||
}
|
||||
case 'passwordEntryInputRejected': {
|
||||
const payload = failure as PasswordEntryInputRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'passwordLoginRejected': {
|
||||
const payload = failure as PasswordLoginRejected;
|
||||
setLoginStatus(
|
||||
`登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'phoneOrPasswordMismatch': {
|
||||
const payload = failure as PhoneOrPasswordMismatch;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('手机号或密码错误');
|
||||
break;
|
||||
}
|
||||
case 'sendCodeInputRejected': {
|
||||
const payload = failure as SendCodeInputRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'sendCodeRejected': {
|
||||
const payload = failure as SendCodeRejected;
|
||||
setLoginStatus(
|
||||
`发送验证码失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeThrottled': {
|
||||
const payload = failure as SmsCodeThrottled;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('发送验证码过于频繁,请稍后再试');
|
||||
break;
|
||||
}
|
||||
case 'phoneLoginInputRejected': {
|
||||
const payload = failure as PhoneLoginInputRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'phoneCodeLoginRejected': {
|
||||
const payload = failure as PhoneCodeLoginRejected;
|
||||
setLoginStatus(
|
||||
`验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeInvalidOrExpired': {
|
||||
const payload = failure as SmsCodeInvalidOrExpired;
|
||||
setLoginStatus(payload.message);
|
||||
case 'smsCodeRejected': {
|
||||
setLoginStatus('验证码错误或已过期');
|
||||
break;
|
||||
}
|
||||
case 'sessionAuthorityRejected': {
|
||||
const payload = failure as SessionAuthorityRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'sessionInvalidated': {
|
||||
setLoginStatus('登录状态已失效,请重新登录');
|
||||
break;
|
||||
}
|
||||
case 'permissionDenied': {
|
||||
const payload = failure as PermissionDenied;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('当前账号没有权限执行此操作');
|
||||
break;
|
||||
}
|
||||
// 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkUnavailable':
|
||||
case 'authNetworkTimeout':
|
||||
case 'authNetworkUnreachable':
|
||||
case 'authServiceUnavailable':
|
||||
case 'unexpectedRejection':
|
||||
case 'authResponseMalformed':
|
||||
case 'authResponseNotJson':
|
||||
case 'authResponseInvalidBody':
|
||||
case 'authResponseMissingRefreshCookie':
|
||||
case 'authResponseMissingUserIdentity':
|
||||
case 'authResponseServerRejected':
|
||||
case 'clientSessionPersistFailed':
|
||||
case 'runtimeSessionInstallFailed':
|
||||
case 'authClientInitFailed':
|
||||
@@ -593,71 +656,98 @@ export function AuthenticatedClient({
|
||||
if (!(error instanceof ClientAuthErrorWrapper)) throw error;
|
||||
const failure = error.error;
|
||||
switch (failure.type) {
|
||||
case 'serverAddressRejected': {
|
||||
const payload = failure as ServerAddressRejected;
|
||||
setLoginStatus(payload.message);
|
||||
// 预期业务拒绝:调用方补上本次操作的上下文,展示后不进错误池。
|
||||
case 'serverAddressEmptyOrTooLong': {
|
||||
setLoginStatus('服务器地址非法: 不能为空或超长');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressNotAUrl': {
|
||||
setLoginStatus('服务器地址非法: 不是合法地址');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressHasCredentials': {
|
||||
setLoginStatus('服务器地址非法: 不能包含用户名或密码');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressHasPathOrQueryOrFragment': {
|
||||
setLoginStatus('服务器地址非法: 不能带路径、查询或片段');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressNotHttps': {
|
||||
setLoginStatus('服务器地址非法: 远程地址必须使用 https');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressUnsupportedScheme': {
|
||||
setLoginStatus('服务器地址非法: 只支持 http 或 https');
|
||||
break;
|
||||
}
|
||||
case 'serverAddressOutsideChannel': {
|
||||
setLoginStatus('服务器地址非法: 不在当前构建渠道范围内');
|
||||
break;
|
||||
}
|
||||
case 'phoneNumberInvalid': {
|
||||
const payload = failure as PhoneNumberInvalid;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('手机号无效: 需为纯数字且不超过 32 位');
|
||||
break;
|
||||
}
|
||||
case 'passwordMissing': {
|
||||
const payload = failure as PasswordMissing;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('密码无效: 不能为空');
|
||||
break;
|
||||
}
|
||||
case 'loginCodeMissing': {
|
||||
const payload = failure as LoginCodeMissing;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('验证码无效: 不能为空');
|
||||
break;
|
||||
}
|
||||
case 'passwordEntryInputRejected': {
|
||||
const payload = failure as PasswordEntryInputRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'passwordLoginRejected': {
|
||||
const payload = failure as PasswordLoginRejected;
|
||||
setLoginStatus(
|
||||
`登录失败: ${payload.serverMessage ?? '服务端拒绝了本次登录'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'phoneOrPasswordMismatch': {
|
||||
const payload = failure as PhoneOrPasswordMismatch;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('手机号或密码错误');
|
||||
break;
|
||||
}
|
||||
case 'sendCodeInputRejected': {
|
||||
const payload = failure as SendCodeInputRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'sendCodeRejected': {
|
||||
const payload = failure as SendCodeRejected;
|
||||
setLoginStatus(
|
||||
`发送验证码失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeThrottled': {
|
||||
const payload = failure as SmsCodeThrottled;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('发送验证码过于频繁,请稍后再试');
|
||||
break;
|
||||
}
|
||||
case 'phoneLoginInputRejected': {
|
||||
const payload = failure as PhoneLoginInputRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'phoneCodeLoginRejected': {
|
||||
const payload = failure as PhoneCodeLoginRejected;
|
||||
setLoginStatus(
|
||||
`验证码登录失败: ${payload.serverMessage ?? '服务端拒绝了本次请求'}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'smsCodeInvalidOrExpired': {
|
||||
const payload = failure as SmsCodeInvalidOrExpired;
|
||||
setLoginStatus(payload.message);
|
||||
case 'smsCodeRejected': {
|
||||
setLoginStatus('验证码错误或已过期');
|
||||
break;
|
||||
}
|
||||
case 'sessionAuthorityRejected': {
|
||||
const payload = failure as SessionAuthorityRejected;
|
||||
setLoginStatus(payload.message);
|
||||
case 'sessionInvalidated': {
|
||||
setLoginStatus('登录状态已失效,请重新登录');
|
||||
break;
|
||||
}
|
||||
case 'permissionDenied': {
|
||||
const payload = failure as PermissionDenied;
|
||||
setLoginStatus(payload.message);
|
||||
setLoginStatus('当前账号没有权限执行此操作');
|
||||
break;
|
||||
}
|
||||
// 系统失败:调用方处理不了,原样抛出 → 全局 unhandledrejection 交给错误池。
|
||||
case 'authNetworkUnavailable':
|
||||
case 'authNetworkTimeout':
|
||||
case 'authNetworkUnreachable':
|
||||
case 'authServiceUnavailable':
|
||||
case 'unexpectedRejection':
|
||||
case 'authResponseMalformed':
|
||||
case 'authResponseNotJson':
|
||||
case 'authResponseInvalidBody':
|
||||
case 'authResponseMissingRefreshCookie':
|
||||
case 'authResponseMissingUserIdentity':
|
||||
case 'authResponseServerRejected':
|
||||
case 'clientSessionPersistFailed':
|
||||
case 'runtimeSessionInstallFailed':
|
||||
case 'authClientInitFailed':
|
||||
|
||||
@@ -41,8 +41,8 @@ function requireInvoke() {
|
||||
*
|
||||
* **信任映射,不做运行时形状嗅探**:Rust 与 TS 同包发布,认证命令的拒绝就是 ts-rs 生成的
|
||||
* `ClientAuthError` 判别联合;出现别的形状属于 Tauri / Rust 侧缺陷,调用方 `switch` 的
|
||||
* `default` 分支仍会把它抛出去上报。包装本身不读变体字段(`message` 之类不保证每个变体都有)、
|
||||
* 不注入上下文、不兜底文案。
|
||||
* `default` 分支仍会把它抛出去上报。包装本身不读变体字段、
|
||||
* 不注入上下文、不拼用户可见文案。
|
||||
*/
|
||||
async function invokeClientAuth<T>(
|
||||
command: string,
|
||||
|
||||
@@ -15,8 +15,8 @@ export type ClientErrorReportContext = {
|
||||
* 认证命令失败的 JS 侧载体:`error` 就是 Rust 的结构化拒绝(ts-rs 生成的 `ClientAuthError`
|
||||
* 判别联合)。
|
||||
*
|
||||
* 本类只把原始拒绝值装成 JS `Error`,不读、不产任何派生值:不读变体上的 `message`(变体不
|
||||
* 保证都有这个字段),不注入 `source` / `action`,`Error.message` 留空。分流只看类型化的变体,
|
||||
* 本类只把原始拒绝值装成 JS `Error`,不读、不产任何派生值:不读变体字段,不注入
|
||||
* `source` / `action`,`Error.message` 留空。分流只看类型化的变体,
|
||||
* **不要用文案判断**;上报上下文由调用 `captureClientError` 时的显式入参决定。
|
||||
*/
|
||||
export class ClientAuthErrorWrapper extends Error {
|
||||
|
||||
Reference in New Issue
Block a user