fix(auth): stop parsing logout response body (avoids ZodError on null data)
Root cause: backend `/auth/logout` returns `{ success: true, data: null }`
(no business payload for a fire-and-forget endpoint). The previous
`AuthApi.logout` ran `unwrap(env)` (which passes null through, since
null is defined) and then `LogoutResponse.fromJson(null)`, which
Zod-parses as `z.object(...)` and throws "Invalid input: expected
object, received null".
The error was caught in `AuthRepository.logout` with
`console.warn(... clearing local anyway)` — logout actually still
worked (local clear ran), but a noisy red ZodError hit the console on
every logout click.
Fix: align `AuthApi.logout` with the existing fire-and-forget pattern
used by `register` and `sendCode` — call `httpClient` and ignore the
response body. Drop the now-unused `LogoutResponse` import.
`AuthRepository.logout` already discards the return value, so the
`Promise<LogoutResponse> → Promise<void>` signature change is safe.
Bundled in this commit (unrelated):
- chat-machine.actors.ts / user-machine.ts: removed stale design-doc
comment blocks (IDE/linter cleanup)
- user-machine.actors.ts: removed redundant `userStorage.clearUserData()`
from `userLogoutActor` — `authRepo.logout` already clears local
user data, so this was a no-op duplicate.
- sidebar-screen.tsx: removed one stale comment line.
This commit is contained in:
@@ -44,7 +44,6 @@ export function SidebarScreen() {
|
|||||||
|
|
||||||
// 等价 Dart: BlocConsumer<UserBloc> 的 listener(profile_view.dart:24-34)
|
// 等价 Dart: BlocConsumer<UserBloc> 的 listener(profile_view.dart:24-34)
|
||||||
// 仅在 currentUser 由非 null 跳到 null 时触发清理
|
// 仅在 currentUser 由非 null 跳到 null 时触发清理
|
||||||
// 注:chat 机器不再需要 `ChatAuthStatusChanged` 通知(已删)—— chat-screen
|
|
||||||
// 通过 `useAuthState()` 主动订阅 loginStatus;这里只需重置 auth + 跳 /chat
|
// 通过 `useAuthState()` 主动订阅 loginStatus;这里只需重置 auth + 跳 /chat
|
||||||
const prevUserRef = useRef<UserView | null>(user.currentUser);
|
const prevUserRef = useRef<UserView | null>(user.currentUser);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request";
|
|||||||
import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
|
import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
|
||||||
import { LoginRequest } from "@/data/dto/auth/login_request";
|
import { LoginRequest } from "@/data/dto/auth/login_request";
|
||||||
import { LoginResponse } from "@/data/dto/auth/login_response";
|
import { LoginResponse } from "@/data/dto/auth/login_response";
|
||||||
import { LogoutResponse } from "@/data/dto/auth/logout_response";
|
|
||||||
import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
|
import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
|
||||||
import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
|
import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
|
||||||
import { RegisterRequest } from "@/data/dto/auth/register_request";
|
import { RegisterRequest } from "@/data/dto/auth/register_request";
|
||||||
@@ -102,13 +101,18 @@ export class AuthApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录
|
* 退出登录(fire-and-forget)
|
||||||
|
*
|
||||||
|
* 后端 logout 端点的 envelope `data` 字段常为 `null`(登出端点没有业务数据),
|
||||||
|
* 不要再用 `unwrap` + `LogoutResponse.fromJson` 解析 —— Zod schema 是 z.object,
|
||||||
|
* 对 null 会抛 ZodError "Invalid input: expected object, received null"。
|
||||||
|
*
|
||||||
|
* 对齐 `register` / `sendCode` 的写法:调通就完事,不解析响应体。
|
||||||
*/
|
*/
|
||||||
async logout(): Promise<LogoutResponse> {
|
async logout(): Promise<void> {
|
||||||
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.logout, {
|
await httpClient<ApiEnvelope<unknown>>(ApiPath.logout, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
return LogoutResponse.fromJson(unwrap(env) as Record<string, unknown>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Chat 状态机:4 个 XState actor
|
* Chat 状态机:4 个 XState actor
|
||||||
*
|
|
||||||
* 从 `chat-machine.ts` 抽出的"Actors"段:
|
|
||||||
* - `loadQuotaActor`(fromPromise,仅游客有意义 —— 拉日配 + 总配)
|
|
||||||
* - `loadHistoryActor`(fromPromise —— local → network → save network to local 3 步)
|
|
||||||
* - `sendMessageHttpActor`(fromPromise,已 wire —— "一次发送 = 一次返回")
|
|
||||||
* - `loadMoreHistoryActor`(fromPromise —— 翻页,不走 local-first)
|
|
||||||
* - `chatWebSocketActor`(fromCallback,长生命周期 —— 仅 userSession 期间 invoke)
|
|
||||||
*
|
|
||||||
* 设计:
|
|
||||||
* - 依赖 `chat-machine.helpers.ts`(纯函数 + 数据加载)
|
|
||||||
* - 依赖 `createChatWebSocket`(`@/core/net/chat-websocket`)
|
|
||||||
* - 保留全链日志(用户要求"不要将其删除")
|
|
||||||
* - 命名约定:`[chat-machine]` 前缀 —— 与 `chat-machine.ts` 的日志对齐
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fromPromise, fromCallback } from "xstate";
|
import { fromPromise, fromCallback } from "xstate";
|
||||||
|
|||||||
@@ -70,5 +70,4 @@ export const userFetchActor = fromPromise<UserView | null>(async () => {
|
|||||||
*/
|
*/
|
||||||
export const userLogoutActor = fromPromise<void>(async () => {
|
export const userLogoutActor = fromPromise<void>(async () => {
|
||||||
await authRepo.logout();
|
await authRepo.logout();
|
||||||
await userStorage.clearUserData();
|
|
||||||
});
|
});
|
||||||
@@ -1,19 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* User 状态机(XState v5)
|
* User 状态机(XState v5)
|
||||||
*
|
|
||||||
* 原始 Dart: lib/ui/user/bloc/user bloc + user state + user event
|
|
||||||
*
|
|
||||||
* 设计要点:
|
|
||||||
* - XState v5 `setup({...}).createMachine({...})` 声明式 API
|
|
||||||
* - HTTP 操作用 `fromPromise` actor(一次性 Promise)→ 见 `user-machine.actors.ts`
|
|
||||||
* - 纯函数 / 数据加载 → 见 `user-machine.helpers.ts`
|
|
||||||
* - 同步状态更新用 `assign` actions(inline 在 setup() 中)
|
|
||||||
* - 保持事件类型名与原 Dart 一致
|
|
||||||
*
|
|
||||||
* 分层:
|
|
||||||
* - helpers: 无 XState 依赖(DTO 映射 + 数据加载)
|
|
||||||
* - actors: 依赖 helpers(异步 actor 实现)
|
|
||||||
* - machine: 依赖 actors(XState setup + 状态图)
|
|
||||||
*/
|
*/
|
||||||
import { setup, assign } from "xstate";
|
import { setup, assign } from "xstate";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user