fix(chat): redirect unauthenticated users from chat
This commit is contained in:
@@ -19,12 +19,14 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthState } from "@/stores/auth/auth-context";
|
||||||
import type { LoginStatus } from "@/data/dto/auth";
|
import type { LoginStatus } from "@/data/dto/auth";
|
||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
||||||
import { AppEnvUtil } from "@/utils/app-env";
|
import { AppEnvUtil } from "@/utils/app-env";
|
||||||
|
import { ROUTES } from "@/router/routes";
|
||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ function dispatchAuthLifecycle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ChatScreen() {
|
export function ChatScreen() {
|
||||||
|
const router = useRouter();
|
||||||
const state = useChatState();
|
const state = useChatState();
|
||||||
const authState = useAuthState();
|
const authState = useAuthState();
|
||||||
const chatDispatch = useChatDispatch();
|
const chatDispatch = useChatDispatch();
|
||||||
@@ -91,37 +94,36 @@ export function ChatScreen() {
|
|||||||
const showExhaustedBanner =
|
const showExhaustedBanner =
|
||||||
isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0;
|
isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
// 屏幕生命周期 + 派初次鉴权生命周期事件
|
// loginStatus 变化 → 派对应鉴权生命周期事件
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
useEffect(() => {
|
|
||||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
|
||||||
return () => {
|
|
||||||
};
|
|
||||||
// 注:mount useEffect 故意不依赖 loginStatus —— 首次 mount 时机是固定的
|
|
||||||
// ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。
|
|
||||||
// loginStatus 后续变化由下方"副作用 ②" 统一处理。
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [chatDispatch]);
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// 副作用 ②:loginStatus 变化 → 派对应鉴权生命周期事件
|
|
||||||
//
|
//
|
||||||
// 规则:
|
// 规则:
|
||||||
// - 首次 mount(prev === null)→ 由 mount useEffect 统一处理
|
// - AuthInit 未完成 → 等,避免初始 notLoggedIn 把 /chat 变成死状态
|
||||||
// - "notLoggedIn" → 派 ChatLogout
|
// - AuthInit 完成后仍 "notLoggedIn" → 清 chat 并回 splash
|
||||||
// - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin
|
// - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!authState.hasInitialized || authState.isLoading) return;
|
||||||
|
|
||||||
const prev = prevLoginStatusRef.current;
|
const prev = prevLoginStatusRef.current;
|
||||||
prevLoginStatusRef.current = authState.loginStatus;
|
|
||||||
if (prev === null) return; // 首次 mount 由 mount useEffect 处理
|
|
||||||
if (prev === authState.loginStatus) return; // 登录态没变
|
if (prev === authState.loginStatus) return; // 登录态没变
|
||||||
|
prevLoginStatusRef.current = authState.loginStatus;
|
||||||
|
|
||||||
|
if (authState.loginStatus === "notLoggedIn") {
|
||||||
|
chatDispatch({ type: "ChatLogout" });
|
||||||
|
router.replace(ROUTES.splash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
||||||
}, [authState.loginStatus, chatDispatch]);
|
}, [
|
||||||
|
authState.hasInitialized,
|
||||||
|
authState.isLoading,
|
||||||
|
authState.loginStatus,
|
||||||
|
chatDispatch,
|
||||||
|
router,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ interface AuthState {
|
|||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
||||||
loginStatus: MachineContext["loginStatus"];
|
loginStatus: MachineContext["loginStatus"];
|
||||||
|
/** 启动时的本地登录态恢复是否已经完成 */
|
||||||
|
hasInitialized: MachineContext["hasInitialized"];
|
||||||
/**
|
/**
|
||||||
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true,
|
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true,
|
||||||
* splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"`
|
* splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"`
|
||||||
@@ -67,12 +69,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
state.matches("loadingEmailLogin") ||
|
state.matches("loadingEmailLogin") ||
|
||||||
state.matches("loadingEmailRegister") ||
|
state.matches("loadingEmailRegister") ||
|
||||||
state.matches("loadingOAuth") ||
|
state.matches("loadingOAuth") ||
|
||||||
|
state.matches("initializing") ||
|
||||||
state.matches("loggingOut") ||
|
state.matches("loggingOut") ||
|
||||||
state.matches("syncingGoogleBackend") ||
|
state.matches("syncingGoogleBackend") ||
|
||||||
state.matches("syncingFacebookBackend") ||
|
state.matches("syncingFacebookBackend") ||
|
||||||
state.matches("loadingGuestLogin"),
|
state.matches("loadingGuestLogin"),
|
||||||
errorMessage: state.context.errorMessage,
|
errorMessage: state.context.errorMessage,
|
||||||
loginStatus: state.context.loginStatus,
|
loginStatus: state.context.loginStatus,
|
||||||
|
hasInitialized: state.context.hasInitialized,
|
||||||
pendingRedirect: state.context.pendingRedirect,
|
pendingRedirect: state.context.pendingRedirect,
|
||||||
}),
|
}),
|
||||||
[state],
|
[state],
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export const authMachine = setup({
|
|||||||
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
|
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
|
hasInitialized: true,
|
||||||
// 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
|
// 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
}),
|
}),
|
||||||
@@ -120,6 +121,7 @@ export const authMachine = setup({
|
|||||||
actions: assign({
|
actions: assign({
|
||||||
errorMessage: ({ event }) =>
|
errorMessage: ({ event }) =>
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -137,6 +139,7 @@ export const authMachine = setup({
|
|||||||
// Skip 点击后已在 splash 里立即跳 /chat;guest login 不再依赖 pendingRedirect。
|
// Skip 点击后已在 splash 里立即跳 /chat;guest login 不再依赖 pendingRedirect。
|
||||||
pendingRedirect: false,
|
pendingRedirect: false,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
@@ -144,6 +147,7 @@ export const authMachine = setup({
|
|||||||
actions: assign({
|
actions: assign({
|
||||||
errorMessage: ({ event }) =>
|
errorMessage: ({ event }) =>
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -164,6 +168,7 @@ export const authMachine = setup({
|
|||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
pendingRedirect: true, // ← 邮箱登录成功 → 跳
|
pendingRedirect: true, // ← 邮箱登录成功 → 跳
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
@@ -171,6 +176,7 @@ export const authMachine = setup({
|
|||||||
actions: assign({
|
actions: assign({
|
||||||
errorMessage: ({ event }) =>
|
errorMessage: ({ event }) =>
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -196,6 +202,7 @@ export const authMachine = setup({
|
|||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
pendingRedirect: true, // ← 注册成功 → 跳
|
pendingRedirect: true, // ← 注册成功 → 跳
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
@@ -203,6 +210,7 @@ export const authMachine = setup({
|
|||||||
actions: assign({
|
actions: assign({
|
||||||
errorMessage: ({ event }) =>
|
errorMessage: ({ event }) =>
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -236,12 +244,16 @@ export const authMachine = setup({
|
|||||||
src: "logout",
|
src: "logout",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "idle",
|
target: "idle",
|
||||||
actions: assign(() => initialState),
|
actions: assign(() => ({
|
||||||
|
...initialState,
|
||||||
|
hasInitialized: true,
|
||||||
|
})),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
target: "idle",
|
target: "idle",
|
||||||
actions: assign(({ event }) => ({
|
actions: assign(({ event }) => ({
|
||||||
...initialState,
|
...initialState,
|
||||||
|
hasInitialized: true,
|
||||||
errorMessage:
|
errorMessage:
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
})),
|
})),
|
||||||
@@ -263,6 +275,7 @@ export const authMachine = setup({
|
|||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
pendingRedirect: true, // ← OAuth sync 成功 → 跳
|
pendingRedirect: true, // ← OAuth sync 成功 → 跳
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
@@ -270,6 +283,7 @@ export const authMachine = setup({
|
|||||||
actions: assign({
|
actions: assign({
|
||||||
errorMessage: ({ event }) =>
|
errorMessage: ({ event }) =>
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -289,6 +303,7 @@ export const authMachine = setup({
|
|||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
pendingRedirect: true, // ← OAuth sync 成功 → 跳
|
pendingRedirect: true, // ← OAuth sync 成功 → 跳
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
onError: {
|
onError: {
|
||||||
@@ -296,6 +311,7 @@ export const authMachine = setup({
|
|||||||
actions: assign({
|
actions: assign({
|
||||||
errorMessage: ({ event }) =>
|
errorMessage: ({ event }) =>
|
||||||
event.error instanceof Error ? event.error.message : String(event.error),
|
event.error instanceof Error ? event.error.message : String(event.error),
|
||||||
|
hasInitialized: true,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export interface AuthState {
|
|||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
/** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */
|
||||||
loginStatus: LoginStatus;
|
loginStatus: LoginStatus;
|
||||||
|
/** 启动时的本地登录态恢复是否已经完成 */
|
||||||
|
hasInitialized: boolean;
|
||||||
/**
|
/**
|
||||||
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true
|
* 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true
|
||||||
* → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转
|
* → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转
|
||||||
@@ -35,5 +37,6 @@ export const initialState: AuthState = {
|
|||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
loginStatus: "notLoggedIn",
|
loginStatus: "notLoggedIn",
|
||||||
|
hasInitialized: false,
|
||||||
pendingRedirect: false,
|
pendingRedirect: false,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user