fix:splash 界面重定向界面问题
This commit is contained in:
@@ -15,8 +15,8 @@
|
|||||||
*
|
*
|
||||||
* 跳转时序:
|
* 跳转时序:
|
||||||
* 1. 派发事件(如 `AuthGuestLoginSubmitted`)
|
* 1. 派发事件(如 `AuthGuestLoginSubmitted`)
|
||||||
* 2. auth machine 进 loading state → API 调用 → onDone → state.isSuccess = true
|
* 2. auth machine 进 loading state → API 调用 → onDone → loginStatus 更新
|
||||||
* 3. useEffect 监听到 isSuccess → 调 chat/user store 初始化 + router.replace("/chat")
|
* 3. useEffect 监听到 isSuccess (= loginStatus !== "notLoggedIn") → 调 chat/user store 初始化 + router.replace("/chat")
|
||||||
*/
|
*/
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
@@ -47,17 +47,10 @@ export function SplashButton() {
|
|||||||
|
|
||||||
// 邮箱/OAuth/游客登录成功跳 /chat + 通知 chat/user 初始化
|
// 邮箱/OAuth/游客登录成功跳 /chat + 通知 chat/user 初始化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// DEBUG:isSuccess 状态透明化
|
|
||||||
console.log("[splash-button] useEffect state.isSuccess", {
|
|
||||||
isSuccess: state.isSuccess,
|
|
||||||
wasSuccess: wasSuccess.current,
|
|
||||||
willRedirect: state.isSuccess && !wasSuccess.current,
|
|
||||||
});
|
|
||||||
if (state.isSuccess && !wasSuccess.current) {
|
if (state.isSuccess && !wasSuccess.current) {
|
||||||
wasSuccess.current = true;
|
wasSuccess.current = true;
|
||||||
chatDispatch({ type: "ChatAuthStatusChanged" });
|
chatDispatch({ type: "ChatAuthStatusChanged" });
|
||||||
userDispatch({ type: "UserInit" });
|
userDispatch({ type: "UserInit" });
|
||||||
console.log("[splash-button] useEffect → router.replace(/chat) [via isSuccess]");
|
|
||||||
router.replace(ROUTES.chat);
|
router.replace(ROUTES.chat);
|
||||||
}
|
}
|
||||||
}, [state.isSuccess, chatDispatch, userDispatch, router]);
|
}, [state.isSuccess, chatDispatch, userDispatch, router]);
|
||||||
|
|||||||
@@ -22,9 +22,6 @@ export function SplashScreen() {
|
|||||||
// - 都没有 → 留在 splash(**不**自动创建游客账号 —— 见 auth-machine.ts 的 checkAuthStatusActor)
|
// - 都没有 → 留在 splash(**不**自动创建游客账号 —— 见 auth-machine.ts 的 checkAuthStatusActor)
|
||||||
//
|
//
|
||||||
// proxy 读不到 localStorage(Edge runtime),所以这条 fallback 必须在 Client 侧。
|
// proxy 读不到 localStorage(Edge runtime),所以这条 fallback 必须在 Client 侧。
|
||||||
//
|
|
||||||
// DEBUG:client 端**不**用 Logger(pino 引入会爆 client bundle),改用 console.log
|
|
||||||
// 临时调试,bug 修完即删。
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
@@ -40,15 +37,7 @@ export function SplashScreen() {
|
|||||||
(loginTok != null && loginTok.length > 0) ||
|
(loginTok != null && loginTok.length > 0) ||
|
||||||
(guestTok != null && guestTok.length > 0);
|
(guestTok != null && guestTok.length > 0);
|
||||||
|
|
||||||
console.log("[splash] useEffect token check", {
|
|
||||||
loginToken: loginTok ? `${loginTok.slice(0, 8)}...` : null,
|
|
||||||
guestToken: guestTok ? `${guestTok.slice(0, 8)}...` : null,
|
|
||||||
hasAny,
|
|
||||||
willRedirect: hasAny && !cancelled,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!cancelled && hasAny) {
|
if (!cancelled && hasAny) {
|
||||||
console.log("[splash] useEffect → router.replace(/chat)");
|
|
||||||
router.replace(ROUTES.chat);
|
router.replace(ROUTES.chat);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
+3
-15
@@ -23,9 +23,6 @@
|
|||||||
* - 函数导出名建议为 `proxy`(可默认导出)。
|
* - 函数导出名建议为 `proxy`(可默认导出)。
|
||||||
*
|
*
|
||||||
* 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。
|
* 用 `src/` 时 proxy 必须放在 `src/` 内(参见 `src-folder.md`)。
|
||||||
*
|
|
||||||
* DEBUG:每条请求都打日志(`hasSession` / `isAuthOnly` / `isProtected` + 决策),
|
|
||||||
* 用于排查"进入 /splash 后仍自动跳 /chat"类路由 bug。bug 修完可考虑保留 INFO 级别 + 删 DEBUG。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
@@ -45,7 +42,7 @@ const SESSION_COOKIE_NAMES = [
|
|||||||
"__Secure-next-auth.session-token",
|
"__Secure-next-auth.session-token",
|
||||||
];
|
];
|
||||||
|
|
||||||
/** proxy 入口日志(Node.js runtime 跑,pino 兼容) */
|
/** proxy 入口日志(Node.js runtime 跑,pino 兼容)—— 排查 / 监控路由用 */
|
||||||
const log = new Logger("Proxy");
|
const log = new Logger("Proxy");
|
||||||
|
|
||||||
function isAuthOnlyRoute(pathname: string): boolean {
|
function isAuthOnlyRoute(pathname: string): boolean {
|
||||||
@@ -66,17 +63,9 @@ export function proxy(request: NextRequest) {
|
|||||||
const hasSession = SESSION_COOKIE_NAMES.some(
|
const hasSession = SESSION_COOKIE_NAMES.some(
|
||||||
(name) => Boolean(request.cookies.get(name)?.value),
|
(name) => Boolean(request.cookies.get(name)?.value),
|
||||||
);
|
);
|
||||||
const isAuthOnly = isAuthOnlyRoute(pathname);
|
|
||||||
const isProtected = isProtectedRoute(pathname);
|
|
||||||
|
|
||||||
// DEBUG:每条请求都打(区分 path 1=proxy 跳 vs path 2=splash useEffect 跳)
|
|
||||||
log.info(
|
|
||||||
{ pathname, hasSession, isAuthOnly, isProtected },
|
|
||||||
"[proxy] request",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 已登录访问未登录专属页 → /chat
|
// 已登录访问未登录专属页 → /chat
|
||||||
if (hasSession && isAuthOnly) {
|
if (hasSession && isAuthOnlyRoute(pathname)) {
|
||||||
log.info(
|
log.info(
|
||||||
{ pathname, target: ROUTES.chat, reason: "logged-in → auth-only" },
|
{ pathname, target: ROUTES.chat, reason: "logged-in → auth-only" },
|
||||||
"[proxy] redirect",
|
"[proxy] redirect",
|
||||||
@@ -87,7 +76,7 @@ export function proxy(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 未登录访问登录态专属页 → /splash(首界面)
|
// 未登录访问登录态专属页 → /splash(首界面)
|
||||||
if (!hasSession && isProtected) {
|
if (!hasSession && isProtectedRoute(pathname)) {
|
||||||
log.info(
|
log.info(
|
||||||
{ pathname, target: ROUTES.splash, reason: "not-logged-in → protected" },
|
{ pathname, target: ROUTES.splash, reason: "not-logged-in → protected" },
|
||||||
"[proxy] redirect",
|
"[proxy] redirect",
|
||||||
@@ -97,7 +86,6 @@ export function proxy(request: NextRequest) {
|
|||||||
return NextResponse.redirect(url, 308);
|
return NextResponse.redirect(url, 308);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info({ pathname }, "[proxy] pass-through");
|
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@ interface AuthState {
|
|||||||
confirmPassword: string;
|
confirmPassword: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
|
/**
|
||||||
|
* 字段名 `isSuccess` 保留(接口兼容),语义改为:
|
||||||
|
* true = 用户**已登录**(loginStatus ∈ {email, google, facebook, apple, guest})
|
||||||
|
* false = 未登录(loginStatus === "notLoggedIn")
|
||||||
|
*
|
||||||
|
* 之前用 `state.matches("success")` 当信号 —— 但 status check 流也会误入
|
||||||
|
* success,触发 splash-button 误跳 /chat。现改用 `loginStatus` 判定。
|
||||||
|
*/
|
||||||
isSuccess: boolean;
|
isSuccess: boolean;
|
||||||
loginStatus: MachineContext["loginStatus"];
|
loginStatus: MachineContext["loginStatus"];
|
||||||
}
|
}
|
||||||
@@ -62,7 +70,8 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
state.matches("loadingOAuth") ||
|
state.matches("loadingOAuth") ||
|
||||||
state.matches("loadingGuestLogin"),
|
state.matches("loadingGuestLogin"),
|
||||||
errorMessage: state.context.errorMessage,
|
errorMessage: state.context.errorMessage,
|
||||||
isSuccess: state.matches("success"),
|
// 改用 loginStatus 判断"已登录"(避开 success state 误触发)
|
||||||
|
isSuccess: state.context.loginStatus !== "notLoggedIn",
|
||||||
loginStatus: state.context.loginStatus,
|
loginStatus: state.context.loginStatus,
|
||||||
}),
|
}),
|
||||||
[state],
|
[state],
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
|
|||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Machine
|
// Machine
|
||||||
|
//
|
||||||
|
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
||||||
|
// **不**再设 `success` state —— 之前曾用 "success" 当"刚成功"信号,
|
||||||
|
// 但 status check 流也会误入 success,触发 splash-button 误跳 /chat。
|
||||||
|
// 现在改用 `loginStatus !== "notLoggedIn"` 作为"已登录"信号(见 auth-context.tsx)。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const authMachine = setup({
|
export const authMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
@@ -220,7 +225,7 @@ export const authMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: "checkAuthStatus",
|
src: "checkAuthStatus",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "success",
|
target: "idle", // ← status check 完回 idle(**不**再假冒 success)
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -241,7 +246,7 @@ export const authMachine = setup({
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: "guestLogin",
|
src: "guestLogin",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "success",
|
target: "idle", // ← 改自 "success"
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -266,7 +271,7 @@ export const authMachine = setup({
|
|||||||
return { email: event.email, password: event.password };
|
return { email: event.email, password: event.password };
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "success",
|
target: "idle", // ← 改自 "success"
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -296,7 +301,7 @@ export const authMachine = setup({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "success",
|
target: "idle", // ← 改自 "success"
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -343,7 +348,7 @@ export const authMachine = setup({
|
|||||||
return { idToken: event.idToken };
|
return { idToken: event.idToken };
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "success",
|
target: "idle", // ← 改自 "success"
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -368,7 +373,7 @@ export const authMachine = setup({
|
|||||||
return { accessToken: event.accessToken };
|
return { accessToken: event.accessToken };
|
||||||
},
|
},
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "success",
|
target: "idle", // ← 改自 "success"
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -383,10 +388,6 @@ export const authMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
success: {
|
|
||||||
// 终态:业务层通过 state.matches("success") 判断
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
* → 业务 token 写入本地 storage
|
* → 业务 token 写入本地 storage
|
||||||
*
|
*
|
||||||
* 此组件**无可见 UI**(返回 null),仅作为 NextAuth session 与 auth state
|
* 此组件**无可见 UI**(返回 null),仅作为 NextAuth session 与 auth state
|
||||||
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后。
|
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后:
|
||||||
*
|
* <AuthProvider> ← 提供 useAuthState / useAuthDispatch
|
||||||
* DEBUG:client 端**不**用 Logger(pino 引入会爆 client bundle),改用 console.log
|
* <OAuthSessionSync /> ← 用上面两个 hook
|
||||||
* 临时调试,bug 修完即删。
|
* </AuthProvider>
|
||||||
*/
|
*/
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
@@ -26,22 +26,6 @@ export function OAuthSessionSync() {
|
|||||||
const { loginStatus } = useAuthState();
|
const { loginStatus } = useAuthState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// DEBUG:每条 session 变化都打(区分 OAuth 残留 session 是否触发 sync)
|
|
||||||
console.log("[OAuthSessionSync] session change", {
|
|
||||||
status,
|
|
||||||
hasSession: !!session,
|
|
||||||
provider: session?.provider,
|
|
||||||
idToken: session?.idToken ? `${session.idToken.slice(0, 8)}...` : null,
|
|
||||||
accessToken: session?.accessToken
|
|
||||||
? `${session.accessToken.slice(0, 8)}...`
|
|
||||||
: null,
|
|
||||||
loginStatus,
|
|
||||||
willDispatch:
|
|
||||||
status === "authenticated" &&
|
|
||||||
!!session?.provider &&
|
|
||||||
loginStatus !== session.provider,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1) NextAuth 还没就绪
|
// 1) NextAuth 还没就绪
|
||||||
if (status !== "authenticated" || !session) return;
|
if (status !== "authenticated" || !session) return;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user