refactor(auth): centralize login status sync
This commit is contained in:
@@ -13,7 +13,7 @@ export interface AuthState {
|
||||
username: string;
|
||||
confirmPassword: string;
|
||||
errorMessage: string | null;
|
||||
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
||||
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱)—— 下游同步器监听此字段的双向变化 */
|
||||
loginStatus: LoginStatus;
|
||||
/** 启动时的本地登录态恢复是否已经完成 */
|
||||
hasInitialized: boolean;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Auth → Chat 同步器
|
||||
*
|
||||
* Chat machine 不直接感知 Auth machine;这里作为根级桥接器,持续监听
|
||||
* loginStatus 变化并派发 chat 鉴权生命周期事件。这样页面组件只负责 UI,
|
||||
* 不再承担全局状态同步职责。
|
||||
*/
|
||||
import { useEffect, useRef } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
import type { LoginStatus } from "@/data/dto/auth";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import { useAuthState } from "@/stores/auth/auth-context";
|
||||
import { PROTECTED_ROUTES, ROUTES, type StaticRoute } from "@/router/routes";
|
||||
|
||||
import { useChatDispatch } from "./chat-context";
|
||||
|
||||
function isProtectedPath(pathname: string): boolean {
|
||||
return PROTECTED_ROUTES.some((route: StaticRoute) => pathname === route);
|
||||
}
|
||||
|
||||
export function ChatAuthSync() {
|
||||
const authState = useAuthState();
|
||||
const chatDispatch = useChatDispatch();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authState.hasInitialized || authState.isLoading) return;
|
||||
|
||||
const prev = prevLoginStatusRef.current;
|
||||
if (prev === authState.loginStatus) return;
|
||||
prevLoginStatusRef.current = authState.loginStatus;
|
||||
|
||||
if (authState.loginStatus === "notLoggedIn") {
|
||||
chatDispatch({ type: "ChatLogout" });
|
||||
if (isProtectedPath(pathname)) {
|
||||
router.replace(ROUTES.splash);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (authState.loginStatus === "guest") {
|
||||
chatDispatch({ type: "ChatGuestLogin" });
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
const tokenR = await AuthStorage.getInstance().getLoginToken();
|
||||
if (!cancelled && tokenR.success && tokenR.data) {
|
||||
chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data });
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [
|
||||
authState.hasInitialized,
|
||||
authState.isLoading,
|
||||
authState.loginStatus,
|
||||
chatDispatch,
|
||||
pathname,
|
||||
router,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* 事件名与原 Dart ChatEvent 对齐。
|
||||
*
|
||||
* 历史:原本有 `ChatAuthStatusChanged`(chat 机器刷新 isGuest)—— 已删。
|
||||
* 鉴权状态变化(loginStatus)由 chat-screen 通过 `useAuthState()` 订阅,
|
||||
* 鉴权状态变化(loginStatus)由 <ChatAuthSync /> 通过 `useAuthState()` 订阅,
|
||||
* chat 机器不感知鉴权。
|
||||
*
|
||||
* 鉴权生命周期事件(本轮新增):
|
||||
@@ -16,7 +16,7 @@
|
||||
* chat-screen 不直接创建 ChatWebSocket / 不读 AuthStorage(除 token 取值)。
|
||||
*/
|
||||
export type ChatEvent =
|
||||
// 鉴权生命周期(用户显式触发登录后由 chat-screen 派)
|
||||
// 鉴权生命周期(登录态变化后由 ChatAuthSync 派)
|
||||
| { type: "ChatGuestLogin" }
|
||||
| { type: "ChatNonGuestLogin"; token: string }
|
||||
| { type: "ChatLogout" }
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* Chat 状态机(XState v5)
|
||||
*
|
||||
* 鉴权解耦(事件驱动):
|
||||
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 chat-screen 派生 loginStatus
|
||||
* chat-screen 只派 3 个事件:
|
||||
* chat 机器不感知鉴权 / 不管 WebSocket —— 由 <ChatAuthSync /> 派生 loginStatus
|
||||
* ChatAuthSync 只派 3 个事件:
|
||||
* - `ChatGuestLogin` → 游客会话(断 WS = 不连)
|
||||
* - `ChatNonGuestLogin { token }` → 非游客会话(连 WS)
|
||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||
@@ -221,6 +221,10 @@ export const chatMachine = setup({
|
||||
// guestSession(游客会话 —— 不连 WS,2 个 init 任务并行)
|
||||
// ========================================================================
|
||||
guestSession: {
|
||||
on: {
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatNonGuestLogin: "#chat.userSession",
|
||||
},
|
||||
initial: "initializing",
|
||||
states: {
|
||||
initializing: {
|
||||
@@ -311,8 +315,6 @@ export const chatMachine = setup({
|
||||
},
|
||||
],
|
||||
// 删除 ChatLoadMoreHistory handler —— 游客无服务端 history,不支持翻页
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatNonGuestLogin: "#chat.userSession",
|
||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
||||
ChatWebSocketConnected: { actions: "setWsConnected" },
|
||||
@@ -355,6 +357,12 @@ export const chatMachine = setup({
|
||||
// - sendingViaWs 重新声明 `ChatWebSocketError` 加 target: "ready"(带 action)
|
||||
// —— XState v5: child handler 替换 parent;复制 action
|
||||
on: {
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatGuestLogin: "#chat.guestSession",
|
||||
ChatNonGuestLogin: {
|
||||
target: "#chat.userSession",
|
||||
reenter: true,
|
||||
},
|
||||
ChatAISentenceReceived: { actions: "appendOrUpdateAISentence" },
|
||||
ChatWebSocketError: { actions: "appendSocketErrorMessage" },
|
||||
ChatWebSocketConnected: { actions: "setWsConnected" },
|
||||
@@ -424,8 +432,6 @@ export const chatMachine = setup({
|
||||
ChatLoadMoreHistory: {
|
||||
target: "loadingMore",
|
||||
},
|
||||
ChatLogout: "#chat.idle",
|
||||
ChatGuestLogin: "#chat.guestSession",
|
||||
ChatQuotaExceeded: { actions: "incrementQuotaExceeded" },
|
||||
// 注:ChatAISentenceReceived / ChatWebSocketError / ChatWebSocketConnected
|
||||
// 已上提到 userSession.on,子级不再声明
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from "./chat-auth-sync";
|
||||
export * from "./chat-context";
|
||||
export * from "./chat-events";
|
||||
export * from "./chat-machine.actors";
|
||||
|
||||
@@ -1,35 +1,39 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Auth → User 启动同步器
|
||||
* Auth → User 同步器
|
||||
*
|
||||
* AuthStatusChecker 只负责恢复 loginStatus;头像等登录用户资料由
|
||||
* UserStorage 持久化在 user machine 这一侧。冷启动直达 /chat 时,
|
||||
* 没有经过 splash/auth 的跳转副作用,因此需要在登录态恢复后主动
|
||||
* hydrate user store。
|
||||
* AuthStatusChecker 只负责恢复 loginStatus;用户资料由 user machine
|
||||
* 管理。这里持续监听登录态变化:
|
||||
* - notLoggedIn → 清空本地 user context
|
||||
* - guest / email / OAuth → 重新 hydrate user store
|
||||
*/
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import type { LoginStatus } from "@/data/dto/auth";
|
||||
import { useAuthState } from "@/stores/auth/auth-context";
|
||||
|
||||
import { useUserDispatch } from "./user-context";
|
||||
|
||||
export function UserAuthSync() {
|
||||
const { loginStatus } = useAuthState();
|
||||
const { hasInitialized, isLoading, loginStatus } = useAuthState();
|
||||
const userDispatch = useUserDispatch();
|
||||
const lastInitializedStatusRef = useRef<string | null>(null);
|
||||
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
||||
|
||||
//TODO 在测位栏界面监听变化,监听登录状态的变化若发生变化,则需要分发 user init 事件
|
||||
useEffect(() => {
|
||||
if (loginStatus === "notLoggedIn" || loginStatus === "guest") {
|
||||
lastInitializedStatusRef.current = null;
|
||||
if (!hasInitialized || isLoading) return;
|
||||
|
||||
const prev = prevLoginStatusRef.current;
|
||||
if (prev === loginStatus) return;
|
||||
prevLoginStatusRef.current = loginStatus;
|
||||
|
||||
if (loginStatus === "notLoggedIn") {
|
||||
userDispatch({ type: "UserClearLocal" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastInitializedStatusRef.current === loginStatus) return;
|
||||
lastInitializedStatusRef.current = loginStatus;
|
||||
userDispatch({ type: "UserInit" });
|
||||
}, [loginStatus, userDispatch]);
|
||||
}, [hasInitialized, isLoading, loginStatus, userDispatch]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user