Browse Source

token过期处理逻辑;restful api 接口返回数据格式

fogwind 1 week ago
parent
commit
b01cc799ce
54 changed files with 509 additions and 443 deletions
  1. 15 2
      README.md
  2. 2 4
      src/actions/addProductToCartAction.ts
  3. 4 5
      src/actions/deleteGuestCookieAction.ts
  4. 1 1
      src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx
  5. 1 1
      src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx
  6. 2 3
      src/app/(checkout)/checkout/continuetopay/page.tsx
  7. 2 4
      src/app/(checkout)/checkout/page.tsx
  8. 47 5
      src/app/(checkout)/error.tsx
  9. 3 3
      src/app/(public)/customer/account/_components/AccountVipPopup.tsx
  10. 4 4
      src/app/(public)/customer/account/mypoints/_components/GetPoints.tsx
  11. 1 1
      src/app/(public)/customer/account/pointlist/page.tsx
  12. 2 2
      src/app/(public)/customer/address/_components/DefaultAddress.tsx
  13. 2 2
      src/app/(public)/customer/address/eidt/[id]/page.tsx
  14. 1 1
      src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx
  15. 1 1
      src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx
  16. 1 0
      src/app/(public)/customer/order/track/order_id/[id]/page.tsx
  17. 1 0
      src/app/(public)/customer/order/view/order_id/[id]/page.tsx
  18. 47 5
      src/app/(public)/error.tsx
  19. 4 5
      src/app/(public)/page.tsx
  20. 2 3
      src/app/(public)/paymentresult/result/page.tsx
  21. 3 2
      src/app/(public)/product/_components/ProductInformation.tsx
  22. 2 2
      src/app/(public)/product/_components/SwitchButton.tsx
  23. 2 2
      src/app/(public)/product/_components/review/ReviewModal.tsx
  24. 11 19
      src/app/api/customer/growth-value/route.ts
  25. 9 17
      src/app/api/customer/orders/[id]/route.ts
  26. 7 16
      src/app/api/customer/orders/route.ts
  27. 7 16
      src/app/api/customer/orders/tracking/route.ts
  28. 7 15
      src/app/api/customer/reward-points/browse-product/route.ts
  29. 8 17
      src/app/api/customer/reward-points/follow-status/route.ts
  30. 6 15
      src/app/api/customer/reward-points/follow/route.ts
  31. 5 15
      src/app/api/customer/reward-points/history/route.ts
  32. 21 54
      src/app/api/customer/token/address/route.ts
  33. 7 15
      src/app/api/home/route.ts
  34. 0 40
      src/app/api/shop/customer-address-gets/route.ts
  35. 0 41
      src/app/api/shop/customer-addresses/route.ts
  36. 7 15
      src/app/api/shop/products/[productId]/reviews/route.ts
  37. 3 3
      src/app/api/shop/reviews/[id]/like/route.ts
  38. 3 3
      src/app/api/shop/reviews/route.ts
  39. 2 0
      src/app/layout.tsx
  40. 3 2
      src/components/common/AddToCartModal/AddToCartModal.tsx
  41. 109 0
      src/components/customer/AuthSessionGuard.tsx
  42. 1 1
      src/components/customer/LoginForm.tsx
  43. 1 1
      src/components/error/ErrorBoundary.tsx
  44. 5 7
      src/graphql/customer/mutations/VerifyCustomer.ts
  45. 20 2
      src/lib/ApolloErrorHandler.ts
  46. 4 1
      src/lib/restApiClient.ts
  47. 9 0
      src/proxy/auth.ts
  48. 5 14
      src/server-service/guestCartTokenService.ts
  49. 6 0
      src/types/next-auth.d.ts
  50. 15 12
      src/types/types.ts
  51. 1 1
      src/utils/auth.ts
  52. 11 0
      src/utils/auth/auth-events.ts
  53. 20 0
      src/utils/auth/auth-helper.ts
  54. 46 43
      src/utils/bagisto/index.ts

+ 15 - 2
README.md

@@ -310,7 +310,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 11. 代码eslint检查修改 -- 已完成
 12. 下单成功后需要重新创建购物车token -- 无需处理
 13. 详情页缓存策略(不要缓存吧)
-14. getSession  过期提示重新登录;token校验
+14. getSession  过期提示重新登录;token校验(见第47条)
 15. checkout 页面 src\components\Portal.tsx (15:33) 会报 document is not defined -- 已解决
 16. redux cartDetail 里的billingAddress 和 shippingAddress 字段改到cart里的billingAddress 和 shippingAddress以及保存完地址后同步到redux
 17. redux 中 cartDetail 异步获取数据 https://chat.deepseek.com/share/76kj30h2cws9jcl4jt  -- 已完成
@@ -350,14 +350,27 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
     -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
 46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)--- 后端已处理
-47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
+47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。
+    因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
     对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
+    ---- 47 条 请求方法已经扩展是否携带token的参数,但是不再添加对于token的有效性校验,以后端接口返回的错误为依据判断有效性
 48. 游客token的创建优化(想在服务端创建写入token)--- 已完成
 
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 
+## 关于token过期
+
+第一种,服务端或者客户端请求接口时,接口返回token过期或者需要需要登录的状态码(401),根据状态码提示用户登录。
+> 客户端通过弹窗提示;服务端通过抛出错误,让error.tsx提示
+
+第二种,通过后端提供的校验token接口结合NextAuth.js主动校验token是否可用
+
+
+需要注意的是,不管哪一种,都要在token失效后,执行nextAuth.js的signOut函数。
+
+
 ## 关于请求接口和错误处理
 接口请求有四种情况:服务端组件里的graphql请求和restful请求;客户端组件里的graphql请求和restful请求。
 ### 服务端组件里的graphql请求

+ 2 - 4
src/actions/addProductToCartAction.ts

@@ -3,17 +3,15 @@
 import {cookies} from "next/headers";
 import { GUEST_CART_TOKEN } from "@/utils/constants";
 import { CREATE_ADD_PRODUCT_IN_CART } from "@/graphql";
-import { getServerSession } from "next-auth";
-import { authOptions } from "@utils/auth";
+import { auth } from "@/utils/auth/auth-helper";
 import { serverGraphqlFetch } from "@utils/bagisto";
-import { BagistoSession } from "@/types/types";
 import {createGuestCartToken,setGuestCookie} from "@/server-service/guestCartTokenService";
 import { AddToCartData,AddToCartVariables } from "@/types/cart/type";
 
 
 export async function addProductToCartAction(param:AddToCartVariables) {
 
-    const authSession = (await getServerSession(authOptions)) as BagistoSession | null;
+    const authSession = await auth();
     const accessToken = authSession?.user?.accessToken;
 
     const cookieStore = await cookies();

+ 4 - 5
src/actions/deleteGuestCookieAction.ts

@@ -2,12 +2,11 @@
 
 import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
 
-export async function deleteGuestCookieAction() {
-        
-    await deleteGuestCookie();
-
+export async function deleteGuestCookieAction(isGuest: boolean = true) {
+    await deleteGuestCookie(isGuest);
+  
     return {
         success: true,
-        msg: ""
+        msg: "",
     };
 }

+ 1 - 1
src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx

@@ -226,7 +226,7 @@ export default function BillingAddressCheckout ({
                     >
                         <span className="text-ly-12">+New Shipping Address</span>
                         {!isGuest && 
-                            <button className="flex-none" title="change address">
+                            <button className="flex-none" title="change address" type="button">
                                 <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
                             </button>
                         }

+ 1 - 1
src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx

@@ -175,7 +175,7 @@ export default function ShippingAddressCheckout({
                 >
                     <span className="text-ly-12">+New Shipping Address</span>
                     {!isGuest && 
-                        <button className="flex-none" title="change address">
+                        <button className="flex-none" title="change address" type="button">
                             <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
                         </button>
                     }

+ 2 - 3
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -1,8 +1,7 @@
 import {Suspense} from "react";
-import { getServerSession } from "next-auth";
 import Link from "next/link";
 import { redirect, RedirectType } from 'next/navigation';
-import { authOptions } from "@utils/auth";
+import { auth } from "@/utils/auth/auth-helper";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
 import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS } from "@/graphql";
@@ -36,7 +35,7 @@ export default async function ContinueToPay({searchParams}: {
   searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
 }) {
     
-    const session = await getServerSession(authOptions);// 游客是null
+    const session = await auth();// 游客是null
     // 游客跳转首页
     if(session === null) {
         redirect('/', RedirectType.replace);

+ 2 - 4
src/app/(checkout)/checkout/page.tsx

@@ -1,10 +1,8 @@
-import { getServerSession } from "next-auth";
-import { authOptions } from "@utils/auth";
 import { redirect, RedirectType } from 'next/navigation';
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import CheckoutWrapper from "./_components/CheckoutWrapper";
 import { GET_CART_ITEM } from "@/graphql";
-
+import { auth } from "@/utils/auth/auth-helper";
 import { GetCartItemData } from "@/types/cart/type";
 
 /**
@@ -21,7 +19,7 @@ export const dynamic = "force-dynamic";
 
 export default async function CheckoutPage() {
 
-    const session = await getServerSession(authOptions); // 游客是null
+    const session = await auth(); // 游客是null
 
 
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({

+ 47 - 5
src/app/(checkout)/error.tsx

@@ -1,18 +1,60 @@
 "use client";
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation';
+import { signOut } from "next-auth/react";
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+//https://nextjs.org/docs/app/api-reference/file-conventions/error
+export default function Error({
+  error, 
+  reset
+}: {
+  reset: () => void;
+  error: Error & { digest?: string };
+}) {
+  const router = useRouter();
+  const dispatch = useAppDispatch();
+  let msg = "There was an issue with our storefront. This could be a temporary issue, please try your action again.";
+  if(error.name === 'UnauthorizedError') {
+    msg = "Authorization Bearer token is required. Will redirect to Login page.";
+  }
+  const clickHandler = () => {
+      if(error.name === 'UnauthorizedError') {
+          router.push('/customer/login')
+      } else {
+        reset();
+      }
+  };
+  useEffect(() => {
+      if (error.name === "UnauthorizedError") {
+            signOut({
+              callbackUrl:"/customer/login",
+              redirect: false,
+            }).then(() => {
+                dispatch(clearCart());
+     
+                setTimeout(() => {
+                  router.push("/customer/login");
+                  router.refresh();
+                }, 100);
+            });
+
+      }
+    // Log the error to an error reporting service
+    console.error(error)
+  }, [error])
 
-export default function Error({ reset }: { reset: () => void }) {
   return (
     <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
       <h2 className="text-xl font-bold">Oh no!</h2>
       <p className="my-2">
-        There was an issue with our storefront. This could be a temporary issue,
-        please try your action again.
+        {msg}
       </p>
       <button
         className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
-        onClick={() => reset()}
+        onClick={clickHandler}
       >
-        Try Again
+        {error.name === 'UnauthorizedError' ? "To Login" : "Try Again"}
       </button>
     </div>
   );

+ 3 - 3
src/app/(public)/customer/account/_components/AccountVipPopup.tsx

@@ -133,9 +133,9 @@ export default function AccountVipPopup({
       try {
         setLoading(true);
         const res = await clientFetch("/api/customer/growth-value");
-        if (res.data.success) {
-          setVipGrothData(res.data.data);
-          console.log("vip成长值数据:", res.data.data);
+        if (res.success) {
+          setVipGrothData(res.data);
+          console.log("vip成长值数据:", res.data);
         }
       } catch (error) {
         console.error("获取vip成长值失败:", error);

+ 4 - 4
src/app/(public)/customer/account/mypoints/_components/GetPoints.tsx

@@ -12,8 +12,8 @@ export default function GetPoints() {
         setLoading(true);
         const res = await clientFetch("/api/customer/reward-points/follow-status");
       
-        setTaskData(res.data.data);
-        console.log("积分任务数据:", res.data.data);
+        setTaskData(res.data);
+        console.log("积分任务数据:", res.data);
       } catch (error) {
         console.error("获取任务状态失败:", error);
       } finally {
@@ -62,12 +62,12 @@ export default function GetPoints() {
         method: "POST",
         body: JSON.stringify({ categorys: item.type }),
       });
-      if(getpointsres.data.success){
+      if(getpointsres.success){
         alert("Points claimed successfully!");
       }
       
       const res = await clientFetch("/api/customer/reward-points/follow-status");
-      setTaskData(res.data.data);
+      setTaskData(res.data);
     } catch (err) {
       console.error("领取失败", err);
       alert("Failed to claim points");

+ 1 - 1
src/app/(public)/customer/account/pointlist/page.tsx

@@ -42,7 +42,7 @@ const PointsDetails = () => {
       console.log("接口返回:", resp);
 
       if (resp.status === 200) {
-        const formData = resp.data?.data || resp.data || { history: [] };
+        const formData = resp?.data || resp.data || { history: [] };
         const newList = formData.history || [];
         // 测试数据
         //  const testList = [...newList, ...newList, ...newList, ...newList];

+ 2 - 2
src/app/(public)/customer/address/_components/DefaultAddress.tsx

@@ -12,8 +12,8 @@ export default function DefaultAddress() {
         `/api/customer/token/address`, ///api/shop/customer-addresses?page=${page}
       );
       console.log("完整返回:", resp.data);
-      resp.data.data= Array.isArray(resp.data.data) ? resp.data.data : [];
-      const sortedList = [...resp.data.data].sort((a, b) => {
+      resp.data= Array.isArray(resp.data) ? resp.data : [];
+      const sortedList = [...resp.data].sort((a, b) => {
         // true(1) 排在 false(0) 前面
         if (a.default_address !== b.default_address) {
           return b.default_address ? 1 : -1;

+ 2 - 2
src/app/(public)/customer/address/eidt/[id]/page.tsx

@@ -51,7 +51,7 @@ const CustomerAddressEditPage = () => {
     });
     const json = await res;
     console.log("提交成功,返回结果:", json);
-    if (json?.data?.success) {
+    if (json?.success) {
       window.location.reload();
     } else {
       alert("error");
@@ -112,7 +112,7 @@ const CustomerAddressEditPage = () => {
       console.log("【完整接口响应】", resp);
 
       // 自动兼容后端 99% 的返回格式
-      const formData = resp.data?.data || resp.data || {};
+      const formData = resp?.data || resp.data || {};
       console.log("✅ 回填数据:", formData);
 
       reset(formData);

+ 1 - 1
src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx

@@ -40,7 +40,7 @@ export default function NewAddressDefault({ countries }: { countries: any[] }) {
     });
     const json = await res;
     console.log("提交成功,返回结果:", json);
-    if(json?.status== 201){
+    if(json?.success){
         router.push("/customer/address/");
     }else{
       alert("error");

+ 1 - 1
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx

@@ -67,7 +67,7 @@ export default function TrackAddress({ address, id }: TrackAddressProps) {
         const res = await clientFetch(
           `/api/customer/orders/tracking?track_number=${id}`,
         );
-        let resData = res?.data;
+        let resData = res;
 
         // ========== 开发mock,上线务必删除这段赋值 ==========
         resData = {

+ 1 - 0
src/app/(public)/customer/order/track/order_id/[id]/page.tsx

@@ -56,6 +56,7 @@ export default async function TrackOrderDetail({ params }: { params: Params }) {
   const res = await restApiFetch<OrderApiResponse>({
     api: `/customer/orders/${orderId}`,
     method: "GET",
+    isRoute: false
   });
   const orderData: OrderData = res.body.data;
   const shippingAddr: AddressData = formatOrderAddress(orderData.shipping_address);

+ 1 - 0
src/app/(public)/customer/order/view/order_id/[id]/page.tsx

@@ -18,6 +18,7 @@ export default async function OrderDetail({ params }: { params: Params }) {
   const res = await restApiFetch<OrderApiResponse>({
     api:`/customer/orders/${orderId}`,
     method:"GET",
+    isRoute: false
   }
   );
 

+ 47 - 5
src/app/(public)/error.tsx

@@ -1,18 +1,60 @@
 "use client";
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation';
+import { signOut } from "next-auth/react";
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+//https://nextjs.org/docs/app/api-reference/file-conventions/error
+export default function Error({
+  error, 
+  reset
+}: {
+  reset: () => void;
+  error: Error & { digest?: string };
+}) {
+  const router = useRouter();
+  const dispatch = useAppDispatch();
+  let msg = "There was an issue with our storefront. This could be a temporary issue, please try your action again.";
+  if(error.name === 'UnauthorizedError') {
+    msg = "Authorization Bearer token is required. Will redirect to Login page.";
+  }
+  const clickHandler = () => {
+      if(error.name === 'UnauthorizedError') {
+          router.push('/customer/login')
+      } else {
+        reset();
+      }
+  };
+  useEffect(() => {
+      if (error.name === "UnauthorizedError") {
+            signOut({
+              callbackUrl:"/customer/login",
+              redirect: false,
+            }).then(() => {
+                dispatch(clearCart());
+     
+                setTimeout(() => {
+                  router.push("/customer/login");
+                  router.refresh();
+                }, 100);
+            });
+
+      }
+    // Log the error to an error reporting service
+    console.error(error)
+  }, [error])
 
-export default function Error({ reset }: { reset: () => void }) {
   return (
     <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
       <h2 className="text-xl font-bold">Oh no!</h2>
       <p className="my-2">
-        There was an issue with our storefront. This could be a temporary issue,
-        please try your action again.
+        {msg}
       </p>
       <button
         className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
-        onClick={() => reset()}
+        onClick={clickHandler}
       >
-        Try Again
+        {error.name === 'UnauthorizedError' ? "To Login" : "Try Again"}
       </button>
     </div>
   );

+ 4 - 5
src/app/(public)/page.tsx

@@ -21,12 +21,11 @@ export default async function Home() {
 const res = await restApiFetch({
   api: `/home`,
   method: "GET",
-  isCookies: true, // 关键开关
-  headers: {
-    "X-Channel": "wap",
-  },
+  isRoute: false,
+  takeAuthorization: false,
+
 });
-console.log("res----------------------",res);
+
 const homeData: HomeApiData = res.body.data;
 
   return (

+ 2 - 3
src/app/(public)/paymentresult/result/page.tsx

@@ -1,8 +1,7 @@
 import {Suspense} from "react";
-import { getServerSession } from "next-auth";
 import { cookies } from 'next/headers';
 import { redirect, RedirectType } from 'next/navigation'
-import { authOptions } from "@utils/auth";
+import { auth } from "@/utils/auth/auth-helper";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {GET_ORDER_DETAILS} from "@/graphql";
 import { OrderDetailsData } from "@/types/customer/type";
@@ -16,7 +15,7 @@ import { GUEST_CART_TOKEN } from "@/utils/constants";
 export default async function SuccessPage({searchParams}: {
   searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
 })  {
-    const session = await getServerSession(authOptions);// 游客是null
+    const session = await auth();// 游客是null
     const isGuest = session === null;
     const cookieStore = await cookies();
     // 登录用户cookie里没有GUEST_CART_ID

+ 3 - 2
src/app/(public)/product/_components/ProductInformation.tsx

@@ -6,7 +6,7 @@ import { ProductOption, ResolvedVariant } from "@/components/catalog/type";
 import { ProductAddToCart } from "@/app/(public)/product/_components/ProductAddToCart";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
-import { redirect, RedirectType } from "next/navigation";
+import { useRouter } from "next/navigation";
 import InstallmentPopup from "./popup/InstallmentPopup";
 import Guide from "./popup/Guide";
 import ProductText from "./ProductText";
@@ -42,6 +42,7 @@ export function ProductInformation({
 }) {
   const { appendProductToCart } = useAddProduct();
   const { showToast } = useCustomToast();
+  const router = useRouter();
 
   // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
   const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(
@@ -213,7 +214,7 @@ export function ProductInformation({
             if(res.data) {
                 const responseData = res.data;
                 if(responseData.success) {
-                    redirect('/checkout?step=address', RedirectType.push);
+                    router.push('/checkout?step=address');
                 }
             }
         }

+ 2 - 2
src/app/(public)/product/_components/SwitchButton.tsx

@@ -82,8 +82,8 @@ const [clientUuid] = useState(() => getUuId());
       );
       console.log('res------------------------------ccc:',res);
 
-      if (res.data.success) {
-        setReviewList(res?.data?.data ?? []);
+      if (res.success) {
+        setReviewList(res?.data ?? []);
         setReviewTotal(res?.pagination?.total ?? 0);
       }
     } catch (err: any) {

+ 2 - 2
src/app/(public)/product/_components/review/ReviewModal.tsx

@@ -45,7 +45,7 @@ async function fetchReviewApi(
   );
   console.log(
     "弹窗内res-=---------------------------------aa:",
-    res?.data?.data,
+    res?.data,
   );
 
   // const mockTotal = 100;
@@ -94,7 +94,7 @@ async function fetchReviewApi(
   //   });
   // }
   // 根据tab筛选数据
-  const filteredList = [...res?.data?.data];
+  const filteredList = [...res?.data];
   const totalReview = res?.pagination?.total;
   // if (tab === "newest") {
   //   // 最新:创建时间倒序

+ 11 - 19
src/app/api/customer/growth-value/route.ts

@@ -1,33 +1,25 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
 
-export async function GET(req: NextRequest) {
+
+
+export async function GET() {
   try {
-    const guestToken = getAuthToken(req);
+
     const response = await restApiFetch<any>({
       api: "/customer/growth-value",
       method: "GET",
       cache: "no-store",
-      guestToken,
-    });
 
-    return NextResponse.json({
-      status: response.status,
-      data: response.body,
     });
 
+    return NextResponse.json(
+      response.body,
+      {status: response.status}
+    );
+
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 9 - 17
src/app/api/customer/orders/[id]/route.ts

@@ -1,12 +1,12 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
+
 type Params = Promise<{ id: string }>;
 export async function GET(req: NextRequest, { params }: { params: Params }) {
   try {
      const { id } = await params; // 解包拿到订单ID
-    const guestToken = getAuthToken(req);
+
 
    const apiUrl = `/customer/orders/${id}`;
 
@@ -16,24 +16,16 @@ export async function GET(req: NextRequest, { params }: { params: Params }) {
       api: apiUrl,
       method: "GET",
       cache: "no-store",
-      guestToken,
-    });
 
-    return NextResponse.json({
-      status: response.status,
-      data: response.body,
     });
 
+    return NextResponse.json(
+      response.body,
+      {status: response.status}
+    );
+
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 7 - 16
src/app/api/customer/orders/route.ts

@@ -1,11 +1,11 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
+
 
 export async function GET(req: NextRequest) {
   try {
-    const guestToken = getAuthToken(req);
+
 
     // 👇 取出所有前端传过来的查询参数
     const searchParams = req.nextUrl.searchParams;
@@ -31,24 +31,15 @@ export async function GET(req: NextRequest) {
       api: apiUrl,
       method: "GET",
       cache: "no-store",
-      guestToken,
+
     });
 
-    return NextResponse.json({
-      status: response.status,
-      data: response.body,
+    return NextResponse.json(response.body,{
+      status: response.status
     });
 
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 7 - 16
src/app/api/customer/orders/tracking/route.ts

@@ -1,11 +1,11 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
+
 
 export async function GET(req: NextRequest) {
   try {
-    const guestToken = getAuthToken(req);
+
 
     // 👇 取出所有前端传过来的查询参数
     const searchParams = req.nextUrl.searchParams;
@@ -29,24 +29,15 @@ export async function GET(req: NextRequest) {
       api: apiUrl,
       method: "GET",
       cache: "no-store",
-      guestToken,
+
     });
 
-    return NextResponse.json({
-      status: response.status,
-      data: response.body,
+    return NextResponse.json(response.body,{
+      status: response.status
     });
 
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 7 - 15
src/app/api/customer/reward-points/browse-product/route.ts

@@ -1,10 +1,10 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
+
 export async function POST(req: NextRequest) {
   try {
-    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+    
     const params = await req.json();
     const response = await restApiFetch<{
       data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
@@ -16,28 +16,20 @@ export async function POST(req: NextRequest) {
       method: "POST",
       cache: "no-store",
       variables: params,
-      guestToken: authorizationToken,
+
     });
     // 打印后端原始返回结构
     console.log(
       "接口原始response.body =",
       JSON.stringify(response.body, null, 2),
     );
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+      
     });
   } catch (error) {
     console.log("/customer/token/address---", error); // 调试用
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 },
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 8 - 17
src/app/api/customer/reward-points/follow-status/route.ts

@@ -1,33 +1,24 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
 
-export async function GET(req: NextRequest) {
+
+
+export async function GET() {
   try {
-    const guestToken = getAuthToken(req);
+
     const response = await restApiFetch<any>({
       api: "/customer/reward-points/follow-status",
       method: "GET",
       cache: "no-store",
-      guestToken,
+
     });
 
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
     });
 
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 6 - 15
src/app/api/customer/reward-points/follow/route.ts

@@ -1,10 +1,10 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
+
 export async function POST(req: NextRequest) {
   try {
-    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+
     const params = await req.json();
     const response = await restApiFetch<{
       data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
@@ -28,28 +28,19 @@ export async function POST(req: NextRequest) {
       method: "POST",
       cache: "no-store",
       variables: params,
-      guestToken: authorizationToken,
+
     });
     // 打印后端原始返回结构
     console.log(
       "接口原始response.body =",
       JSON.stringify(response.body, null, 2),
     );
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
     });
   } catch (error) {
     console.log("/customer/token/address---", error); // 调试用
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 },
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 5 - 15
src/app/api/customer/reward-points/history/route.ts

@@ -1,11 +1,10 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
 
 export async function GET(req: NextRequest) {
   try {
-    const guestToken = getAuthToken(req);
+
 
     // 👇 取出所有前端传过来的查询参数
     const searchParams = req.nextUrl.searchParams;
@@ -31,24 +30,15 @@ export async function GET(req: NextRequest) {
       api: apiUrl,
       method: "GET",
       cache: "no-store",
-      guestToken,
+
     });
 
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+      
     });
 
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
 
     return NextResponse.json(
       {

+ 21 - 54
src/app/api/customer/token/address/route.ts

@@ -1,11 +1,10 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
 // import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
 export async function GET(req: NextRequest) {
   try {
-    const guestToken = getAuthToken(req);
+
     // 从url取查询参数 id
     const addressId = req.nextUrl.searchParams.get("id");
 
@@ -19,22 +18,14 @@ export async function GET(req: NextRequest) {
       api: apiUrl, //
       method: "GET",
       cache: "no-store",
-      guestToken,
+
     });
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+
     });
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 },
-      );
-    }
+
 
     return NextResponse.json(
       {
@@ -47,7 +38,7 @@ export async function GET(req: NextRequest) {
 }
 export async function POST(req: NextRequest) {
   try {
-    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+
     const params = await req.json();
     const response = await restApiFetch<{
       data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
@@ -68,28 +59,20 @@ export async function POST(req: NextRequest) {
       method: "POST",
       cache: "no-store",
       variables: params,
-      guestToken: authorizationToken,
+
     });
     // 打印后端原始返回结构
     console.log(
       "接口原始response.body =",
       JSON.stringify(response.body, null, 2),
     );
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+
     });
   } catch (error) {
     console.log("/customer/token/address---", error); // 调试用
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 },
-      );
-    }
+
 
     return NextResponse.json(
       {
@@ -102,7 +85,7 @@ export async function POST(req: NextRequest) {
 }
 export async function PUT(req: NextRequest) {
   try {
-    const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+
     const params = await req.json();
     // 从url取查询参数 id
     const addressId = req.nextUrl.searchParams.get("id");
@@ -125,28 +108,20 @@ export async function PUT(req: NextRequest) {
       method: "PUT",
       cache: "no-store",
       variables: params,
-      guestToken: authorizationToken,
+
     });
     // 打印后端原始返回结构
     console.log(
       "接口原始response.body =",
       JSON.stringify(response.body, null, 2),
     );
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+
     });
   } catch (error) {
     console.log("/customer/token/address---", error); // 调试用
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 },
-      );
-    }
+
 
     return NextResponse.json(
       {
@@ -159,28 +134,20 @@ export async function PUT(req: NextRequest) {
 }
 export async function DELETE(req: NextRequest) {
   try {
-    const guestToken = getAuthToken(req);
+
     const addressId = req.nextUrl.searchParams.get("id");
     const response = await restApiFetch<any>({
       api: `/customer/token/address/${addressId}`, //
       method: "DELETE",
       cache: "no-store",
-      guestToken,
+
     });
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+
     });
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 },
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 7 - 15
src/app/api/home/route.ts

@@ -1,11 +1,11 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
-export async function GET(req: NextRequest) {
+
+
+export async function GET() {
   try {
     //  const { id } = await params; // 解包拿到订单ID
-    const guestToken = getAuthToken(req);
+  
 
    const apiUrl = `/home`;
 
@@ -15,7 +15,7 @@ export async function GET(req: NextRequest) {
       api: apiUrl,
       method: "GET",
       cache: "no-store",
-      guestToken,
+
     });
 
     return NextResponse.json({
@@ -24,15 +24,7 @@ export async function GET(req: NextRequest) {
     });
 
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 0 - 40
src/app/api/shop/customer-address-gets/route.ts

@@ -1,40 +0,0 @@
-import { NextRequest, NextResponse } from "next/server";
-import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
-// import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
-export async function GET(req: NextRequest) {
-    try {
-        const guestToken = getAuthToken(req);
-        // 从查询参数取 page,默认 1
-        const id = req.nextUrl.searchParams.get("id") || "1";
-        const response = await restApiFetch<any>({
-            api: `/shop/customer-address-gets?id=${id}`,
-            method:'GET',
-            cache:'no-store',
-            guestToken,
-        });
-        return NextResponse.json({
-            status: response.status,
-            data: response.body,
-        });
-    } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
-        return NextResponse.json(
-            {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
-            },
-            { status: 500 }
-        );
-    }
-}

+ 0 - 41
src/app/api/shop/customer-addresses/route.ts

@@ -1,41 +0,0 @@
-import { NextResponse,NextRequest } from "next/server";
-import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
-// import { NextRequestHint } from "next/dist/server/web/adapter";
-// import type { GiftListBody,FetchWrap  } from '@/types/api/gift/lists';
-export async function GET(req: NextRequest) {
-    try {
-        const guestToken = getAuthToken(req);
-        // 从查询参数取 page,默认 1
-        const page = req.nextUrl.searchParams.get("page") || "1";
-        const response = await restApiFetch<any>({
-            api: `/shop/customer-addresses?page=${page}`,
-            method:'GET',
-            cache:'no-store',
-            guestToken,
-        });
-        return NextResponse.json({
-            status: response.status,
-            data: response.body,
-        });
-    } catch (error) {
-        if (isBagistoError(error)) {
-            return NextResponse.json(
-                {
-                    data: null,
-                    error: error.cause ?? error,
-                },
-                { status: 200 }
-            );
-        }
-
-        return NextResponse.json(
-            {
-                message: "Network error",
-                error: error instanceof Error ? error.message : error,
-            },
-            { status: 500 }
-        );
-    }
-}

+ 7 - 15
src/app/api/shop/products/[productId]/reviews/route.ts

@@ -1,14 +1,14 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { isBagistoError } from "@/utils/type-guards";
-import { getAuthToken } from "@/utils/helper";
+
+
 type Params = Promise<{ productId: string }>;
 
 export async function GET(req: NextRequest, { params }: { params: Params }) {
   try {
     const { productId } = await params;
     const searchParams = req.nextUrl.searchParams;
-    const guestToken = getAuthToken(req);
+
     const response = await restApiFetch<any>({
       api: `/shop/products/${productId}/reviews`,
       method: "GET",
@@ -20,23 +20,15 @@ export async function GET(req: NextRequest, { params }: { params: Params }) {
         sort:searchParams.get("sort") ?? 'all', 
         client_id:searchParams.get("client_id") ?? '', 
       },
-       guestToken,
+
     });
 
-    return NextResponse.json({
+    return NextResponse.json(response.body,{
       status: response.status,
-      data: response.body,
+
     });
   } catch (error) {
-    if (isBagistoError(error)) {
-      return NextResponse.json(
-        {
-          data: null,
-          error: error.cause ?? error,
-        },
-        { status: 200 }
-      );
-    }
+
 
     return NextResponse.json(
       {

+ 3 - 3
src/app/api/shop/reviews/[id]/like/route.ts

@@ -1,11 +1,11 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { getAuthToken } from "@/utils/helper";
+
 type RouteParams = Promise<{ id: string }>;
 export async function POST(req: NextRequest, {params}: { params: RouteParams }) {
     try {
         const { id } = await params;
-        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+
          const bodyData = await req.json();
          const api =   `/shop/reviews/${id}/like`;
         console.log("bodyData,api :",bodyData,api);
@@ -20,7 +20,7 @@ export async function POST(req: NextRequest, {params}: { params: RouteParams })
             method:'POST',
             cache:'no-store',
             variables: bodyData,
-            guestToken: authorizationToken,
+
         });
         // 打印后端原始返回结构
         console.log('接口原始response.body =', JSON.stringify(response.body, null, 2));

+ 3 - 3
src/app/api/shop/reviews/route.ts

@@ -1,10 +1,10 @@
 import { NextRequest, NextResponse } from "next/server";
 import { restApiFetch } from "@/utils/bagisto";
-import { getAuthToken } from "@/utils/helper";
+
 
 export async function POST(req: NextRequest) {
     try {
-        const authorizationToken = getAuthToken(req); // 获取headers中的Authorization的值
+
         const params = await req.json();
         const response = await restApiFetch<{
             data: any; // 这个是返回结果的数据类型,暂时写成any,具体看后端反的数据结构再改成确定的类型1
@@ -16,7 +16,7 @@ export async function POST(req: NextRequest) {
             method:'POST',
             cache:'no-store',
             variables: params,
-            guestToken: authorizationToken,
+
         });
         // 打印后端原始返回结构
         console.log('接口原始response.body =', JSON.stringify(response.body, null, 2));

+ 2 - 0
src/app/layout.tsx

@@ -16,6 +16,7 @@ import { GetCartItemData } from "@/types/cart/type";
 import { Country } from "@/types/checkout/type";
 import { CurrenciesData } from "@/types/currency/type";
 import {CURRENT_CURRENCY,CURRENT_LOCAL,CURRENT_CHANNEL} from "@/utils/constants";
+import AuthSessionGuard from "@/components/customer/AuthSessionGuard";
 
 // Locale revision marker — required for SSR a11y locale sync (Next.js i18n).
 // Removing this breaks server-rendered locale negotiation. Do not edit.
@@ -152,6 +153,7 @@ export default async function RootLayout({
                 <GlobalProviders>
                   {children}
                   <AddToCartModalWrapper />
+                  <AuthSessionGuard />
                 </GlobalProviders>
               </ReduxProvider>
               </ConfigProvider>

+ 3 - 2
src/components/common/AddToCartModal/AddToCartModal.tsx

@@ -1,7 +1,7 @@
 "use client";
 
 import { useState, useMemo } from "react";
-import { redirect, RedirectType } from 'next/navigation'
+import { useRouter } from "next/navigation";
 import { useAppDispatch, useAppSelector } from "@/store/hooks";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { 
@@ -36,6 +36,7 @@ import {
 } from "@/utils/variantTools";
 
 export default function AddToCartModal() {
+    const router = useRouter();
     const dispatch = useAppDispatch();
     const {isOpen, product} = useAppSelector((state) => state.addToCartDialog);
 
@@ -224,7 +225,7 @@ export default function AddToCartModal() {
             if(res && !res.error) {
                 const responseData = res.data;
                 if(responseData && responseData.success) {
-                    redirect('/checkout', RedirectType.push);
+                    router.push('/checkout');
                 }
             }
         }

+ 109 - 0
src/components/customer/AuthSessionGuard.tsx

@@ -0,0 +1,109 @@
+"use client";
+
+import {
+    useCallback,
+    useEffect,
+    useRef,
+} from "react";
+import { signOut } from "next-auth/react";
+import { useApolloClient } from "@apollo/client/react";
+import {
+    AUTH_EXPIRED_EVENT,
+} from "@/utils/auth/auth-events";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import { useRouter } from 'next/navigation';
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+
+export default function AuthSessionGuard() {
+
+    const apolloClient = useApolloClient();
+    const router = useRouter();
+    const dispatch = useAppDispatch();
+
+    /**
+     * signOut 正在执行
+     */
+    const signingOutRef = useRef(false);
+
+    const handleAuthExpired = useCallback(
+        async () => {
+
+            /**
+             * 防止重复 signOut
+             */
+            if (signingOutRef.current) {
+                return;
+            }
+
+            signingOutRef.current = true;
+
+            try {
+
+                /**
+                 * 1. 清除 NextAuth session
+                 */
+                await signOut({
+                    redirect: false,
+                });
+                dispatch(clearCart());
+                /**
+                 * 2. 清除 Apollo Cache
+                 *
+                 * 防止旧用户的数据残留。
+                 */
+                await apolloClient.clearStore();
+                confirmDialog({
+                    title: "Unauthorized",
+                    content: 'Authorization Bearer token is required. Please login!',
+                    noCancel: true,
+                }).then(() => {
+                    router.replace('/customer/login')
+                });
+
+            } catch (error) {
+
+                console.error(
+                    "[AuthSessionGuard] Failed to handle auth expiration:",
+                    error
+                );
+
+            } finally {
+
+                /**
+                 * signOut 锁释放。
+                 *
+                 * 注意:
+                 * 这里不是“恢复登录状态”,
+                 * 只是释放并发锁。
+                 */
+                signingOutRef.current = false;
+            }
+        },
+        [apolloClient]
+    );
+
+    useEffect(() => {
+
+        const handleAuthExpiredEvent = () => {
+            void handleAuthExpired();
+        };
+
+        window.addEventListener(
+            AUTH_EXPIRED_EVENT,
+            handleAuthExpiredEvent
+        );
+
+        return () => {
+
+            window.removeEventListener(
+                AUTH_EXPIRED_EVENT,
+                handleAuthExpiredEvent
+            );
+
+        };
+
+    }, [handleAuthExpired]);
+
+    return null;
+}

+ 1 - 1
src/components/customer/LoginForm.tsx

@@ -54,7 +54,7 @@ export default function LoginForm() {
         if(cart) {
           await mergeCartAction();
         } else {
-          await deleteGuestCookieAction();
+          await deleteGuestCookieAction(false);
           await getCartDetail();
         }
 

+ 1 - 1
src/components/error/ErrorBoundary.tsx

@@ -12,7 +12,7 @@ interface State {
   hasError: boolean;
   error?: Error;
 }
-
+//https://zh-hans.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
 export class ErrorBoundary extends Component<Props, State> {
   public state: State = {
     hasError: false,

+ 5 - 7
src/graphql/customer/mutations/VerifyCustomer.ts

@@ -1,12 +1,10 @@
-import { gql } from "@apollo/client";
+import { gql, TypedDocumentNode } from "@apollo/client";
+import {CreateVerifyTokenData} from "@/types/types";
 
-export const VERIFY_CUSTOMER = gql`
-  mutation verifyCustomer($token: String!, $clientMutationId: String) {
+export const VERIFY_CUSTOMER: TypedDocumentNode<CreateVerifyTokenData> = gql`
+  mutation verifyCustomer {
     createVerifyToken(
-      input: {
-        token: $token
-        clientMutationId: $clientMutationId
-      }
+      input: {}
     ) {
       verifyToken {
         id

+ 20 - 2
src/lib/ApolloErrorHandler.ts

@@ -7,9 +7,21 @@ import {
   UnconventionalError,
 } from "@apollo/client/errors";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import { emitAuthExpired } from "@/utils/auth/auth-events";
 // about apollo client error handle https://www.apollographql.com/docs/react/data/error-handling
 // Comprehensive error handling example.
 
+function is401(error: CombinedGraphQLErrors) {
+    let res = false;
+    for(let i = 0; i < error.errors.length; i++) {
+        if(error.errors[i].extensions?.status === 401) {
+            res = true;
+            break;
+        }
+    }
+    return res;
+}
+
 export function handleApolloBusinessError(error: unknown, callback: (err:CombinedGraphQLErrors) => void) {
   if (CombinedGraphQLErrors.is(error)) {
     // Handle GraphQL errors
@@ -18,7 +30,8 @@ export function handleApolloBusinessError(error: unknown, callback: (err:Combine
     //     `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
     //   )
     // );
-    callback(error);
+    if(!is401(error)) callback(error);
+    
     // console.log('CombinedGraphQLErrors--',error.message);
 
   }
@@ -27,7 +40,12 @@ export function handleApolloBusinessError(error: unknown, callback: (err:Combine
 // 已知有一种情况graphql的响应状态码是401,即请求接口的时候请求头缺少X-STOREFRONT-KEY
 export function handleErrorForErrorLink(error: unknown) {
   // CombinedGraphQLErrors 交给具体的业务自己捕获处理
-  if (CombinedGraphQLErrors.is(error)) return;
+  if (CombinedGraphQLErrors.is(error)) {
+    if(is401(error)) {
+       emitAuthExpired();
+    }
+    return
+  }
   if (CombinedProtocolErrors.is(error)) {
     // CombinedProtocolErrors 是GraphQL 传输协议层错误,该项目中遇不到
     // Handle multipart subscription protocol errors

+ 4 - 1
src/lib/restApiClient.ts

@@ -1,6 +1,6 @@
 'use client';
 /**前端客户端组件调rest api 接口 */
-
+import { emitAuthExpired } from "@/utils/auth/auth-events";
 export async function clientFetch<T = any>(apiUrl: string, options: RequestInit = {}): Promise<T> {
     // 请求的是nextjs的代理接口
 
@@ -18,6 +18,9 @@ export async function clientFetch<T = any>(apiUrl: string, options: RequestInit
     const response = await fetch(apiUrl, options);
     const result = await response.json();
     console.log('response -- ', response);
+    if(response.status === 401) { // Unauthorized 需要登录
+        emitAuthExpired();
+    }
     // if(!response.ok) { // 非2xx状态
     //     // 
     // }

+ 9 - 0
src/proxy/auth.ts

@@ -20,6 +20,15 @@ export async function handleAuth(
         return response;
     }
 
+    /**
+     * Server Action 请求不能按照页面访问进行 redirect。
+     */
+    const isServerAction = request.headers.has("Next-Action");
+
+    if (isServerAction) {
+        return response;
+    }
+
     const token = await getToken({
         req: request,
         secret: process.env.NEXTAUTH_SECRET

+ 5 - 14
src/server-service/guestCartTokenService.ts

@@ -1,25 +1,16 @@
 import {cookies} from "next/headers";
-import { getServerSession } from "next-auth";
-import { authOptions } from "@utils/auth";
-import { BagistoSession } from "@/types/types";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import { CREATE_CART_TOKEN } from "@/graphql";
 import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST, GUEST_COOKIE_OPTION } from "@/utils/constants";
 import { CreateCartTokenData } from "@/types/cart/type";
 
 
-export async function deleteGuestCookie() {
-    const authSession = (await getServerSession(authOptions)) as BagistoSession | null;
-    const accessToken = authSession?.user?.accessToken;
-    const isGuest = accessToken ? false : true;
+export async function deleteGuestCookie(isGuest: boolean = true) {
     const cookieStore = await cookies();
-    if(isGuest) {
-        cookieStore.delete(GUEST_CART_TOKEN);
-        cookieStore.delete(GUEST_CART_ID);
-        cookieStore.set(IS_GUEST, "true");
-    } else {
-        cookieStore.set(IS_GUEST, "false");
-    }
+
+    cookieStore.delete(GUEST_CART_TOKEN);
+    cookieStore.delete(GUEST_CART_ID);
+    cookieStore.set(IS_GUEST, isGuest ? "true" : "false");
 
 }
 

+ 6 - 0
src/types/next-auth.d.ts

@@ -11,11 +11,17 @@ declare module "next-auth" {
   interface Session {
     user: {
       id?: string;
+      email?: string | null;
+      name?: string | null;
+      image?: string | null;
+      // apiToken?: string;
+      role?: string;
       firstname?: string;
       lastname?: string;
       token?: string;
       accessToken?: string;
     } & DefaultSession['user'];
+    expires?: string;
   }
 }
 

+ 15 - 12
src/types/types.ts

@@ -1,5 +1,5 @@
 import { SVGProps } from "react";
-import { Session } from "next-auth";
+
 
 export interface LogoutData { 
   createLogout: { 
@@ -9,17 +9,7 @@ export interface LogoutData {
     } 
   } 
 }
-export interface BagistoSession extends Session {
-  user: {
-    id: string;
-    email?: string | null;
-    name?: string | null;
-    image?: string | null;
-    accessToken?: string;
-    apiToken?: string;
-    role?: string;
-  };
-}
+
 
 export type Maybe<T> = T | null;
 
@@ -622,6 +612,19 @@ export type CreateUserResponse = {
     }
 };
 
+export interface CreateVerifyTokenData {
+  createVerifyToken: {
+      verifyToken: {
+        id: number;
+        firstName: string;
+        lastName: string;
+        email: string;
+        isValid: boolean;
+        message: string;
+      }
+  }
+}
+
 export type BagistoUser = {
   id: string;
   _id: number;

+ 1 - 1
src/utils/auth.ts

@@ -83,7 +83,7 @@ export const authOptions: NextAuthOptions = {
       session.user = {
         ...session.user,
         id: token.id || "",
-        apiToken: token.apiToken,
+        // apiToken: token.apiToken,
         accessToken: token.accessToken,
         role: token.role,
       };

+ 11 - 0
src/utils/auth/auth-events.ts

@@ -0,0 +1,11 @@
+export const AUTH_EXPIRED_EVENT = "app:auth-expired";
+
+export function emitAuthExpired() {
+    if (typeof window === "undefined") {
+        return;
+    }
+
+    window.dispatchEvent(
+        new CustomEvent(AUTH_EXPIRED_EVENT)
+    );
+}

+ 20 - 0
src/utils/auth/auth-helper.ts

@@ -0,0 +1,20 @@
+import type {
+  GetServerSidePropsContext,
+  NextApiRequest,
+  NextApiResponse,
+} from "next";
+import { getServerSession } from "next-auth";
+import { authOptions } from "@utils/auth";
+
+
+
+
+// Use it in server contexts
+export function auth(
+  ...args:
+    | [GetServerSidePropsContext["req"], GetServerSidePropsContext["res"]]
+    | [NextApiRequest, NextApiResponse]
+    | []
+) {
+  return getServerSession(...args, authOptions)
+}

+ 46 - 43
src/utils/bagisto/index.ts

@@ -4,14 +4,14 @@ import { NextRequest, NextResponse } from "next/server";
 import {
   BagistoCreateUserOperation,
   BagistoProductInfo,
-  BagistoSession,
   BagistoUser,
   ImageInfo,
 } from "@/types/types";
 import {
   HIDDEN_PRODUCT_TAG,
 } from "../constants";
-import { getServerSession } from "next-auth";
+import { auth } from "@/utils/auth/auth-helper";
+// import { getToken,decode } from "next-auth/jwt";
 import {
   CUSTOMER_REGISTRATION,
   FORGET_PASSWORD,
@@ -89,9 +89,24 @@ async function getBaseHeader() {
 export async function getAuthorizationToken(): Promise<{token: string | null; isGuest: boolean;}> {
   
   // 登录用户从nextAuth里获取
-  const authSession = (await getServerSession(
-        authOptions,
-      )) as BagistoSession | null;
+  /*
+  const tokenCookie =
+      cookieStore.get("next-auth.session-token")
+      ?? cookieStore.get("__Secure-next-auth.session-token");
+
+  if (tokenCookie?.value) {
+      const token = await decode({
+          token: tokenCookie.value,
+          secret: process.env.NEXTAUTH_SECRET!,
+      });
+          console.log('getAuthorizationToken decode ===============',token);
+      return {
+        token: token?.accessToken ?? '',
+        isGuest: false,
+      };
+  }
+  */
+  const authSession = await auth();
   const accessToken = authSession?.user?.accessToken;
   if (accessToken) {
     return {
@@ -118,7 +133,9 @@ export async function restApiFetch<T>({
   headers,
   tags,
   variables,
+  isRoute = true,
   revalidate = 60,
+  takeAuthorization = true, // 是否携带token,默认携带
 }: {
   api: string;
   method: "POST" | "GET" | "PUT" | "DELETE";
@@ -126,37 +143,25 @@ export async function restApiFetch<T>({
   headers?: HeadersInit | Record<string, string>;
   tags?: string[];
   variables?: ExtractVariables<T>;
-  isCookies?: boolean;
+  isRoute?: boolean; // 是否是在route handle中调用
   guestToken?: string;
   revalidate?: number;
+  takeAuthorization?: boolean;
 }): Promise<{ status: number; body: ExtractRestFulData<T> } | never> {
   try {
     const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
     const url = new URL(apiUrl);
 
-    const tokenRes = await getAuthorizationToken();
+    
 
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
-    /*
-    let accessToken: string | undefined = undefined;
-
-    if (isCookies) {
-      const sessions = (await getServerSession(
-        authOptions,
-      )) as BagistoSession | null;
-      accessToken = sessions?.user?.accessToken;
-    }
 
-
-    if (accessToken) {
-      baseHeaders.Authorization = `Bearer ${accessToken}`;
-    } else if (guestToken) {
-      baseHeaders.Authorization = `Bearer ${guestToken}`;
-    }
-    */
-    if (tokenRes.token) {
-      baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
+    if(takeAuthorization) {
+      const tokenRes = await getAuthorizationToken();
+      if (tokenRes.token) {
+        baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
+      }
     }
 
 
@@ -193,6 +198,14 @@ export async function restApiFetch<T>({
 
     const body = await result.json();
     console.log('restApiFetch --- body:', body)
+    if(!isRoute) {
+      if(result.status === 401) {
+        const err = new Error('Authorization Bearer token is required. Please Login.');//new Error(body.message || 'Authorization Bearer token is required');
+        err.name = "UnauthorizedError";
+        throw err;
+      }
+      
+    } 
     return { status: result.status, body };
   } catch (e) {
     throw e;
@@ -257,11 +270,17 @@ export async function serverGraphqlFetch<
     });
 
     const body = await result.json();
-    console.log('serverGraphqlFetch --- body:',body);
+    const err = body.errors?.[0] ?? null;
+    if(err && err.extensions.status === 401) {
+        // "UNAUTHENTICATED"
+        const err = new Error('Authorization Bearer token is required. Please Login.');
+        err.name = "UnauthorizedError";
+        throw err;
+    }
     return {
       status:result.status,
       data:body.data ?? null,
-      error:body.errors?.[0] ?? null
+      error: err
     }
 
   } catch (e) {
@@ -299,23 +318,7 @@ export async function bagistoFetch<T>({
     
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
-    /*
-    let accessToken: string | undefined = undefined;
 
-    if (isCookies) {
-
-      const sessions = (await getServerSession(
-        authOptions,
-      )) as BagistoSession | null;
-      accessToken = sessions?.user?.accessToken;
-    }
-    
-    if (accessToken) {
-      baseHeaders.Authorization = `Bearer ${accessToken}`;
-    } else if (guestToken) {
-      baseHeaders.Authorization = `Bearer ${guestToken}`;
-    }
-    */
     if(takeAuthorization) {
       const tokenRes = await getAuthorizationToken();
       if (tokenRes.token) {