index.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. import { cookies, headers } from "next/headers";
  2. import { revalidatePath } from "next/cache";
  3. import { NextRequest, NextResponse } from "next/server";
  4. import {
  5. BagistoCreateUserOperation,
  6. BagistoProductInfo,
  7. BagistoSession,
  8. BagistoUser,
  9. ImageInfo,
  10. } from "@/types/types";
  11. import {
  12. HIDDEN_PRODUCT_TAG,
  13. } from "../constants";
  14. import { getServerSession } from "next-auth";
  15. import {
  16. CUSTOMER_LOGOUT,
  17. CUSTOMER_REGISTRATION,
  18. FORGET_PASSWORD,
  19. } from "@/graphql/customer/mutations";
  20. import { print, DocumentNode } from "graphql";
  21. import {
  22. GRAPHQL_URL,
  23. REST_API_URL,
  24. } from "@/utils/constants/server";
  25. import {
  26. GUEST_CART_TOKEN,
  27. IS_GUEST,
  28. CURRENT_CURRENCY,
  29. CURRENT_LOCAL,
  30. CURRENT_CHANNEL,
  31. } from "@/utils/constants";
  32. import {
  33. GET_FOOTER,
  34. GET_THEME_CUSTOMIZATION,
  35. PAGE_BY_URL_KEY,
  36. } from "@/graphql";
  37. import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations";
  38. import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
  39. import { authOptions } from "@utils/auth";
  40. import { RegisterInputs } from "@components/customer/RegistrationForm";
  41. import {
  42. GetFooterResponse,
  43. ThemeCustomizationResult,
  44. ThemeCustomizationResponse,
  45. PageData,
  46. } from "@/types/theme/theme-customization";
  47. import { decodeJWT } from "@/utils/jwt-cookie";
  48. import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
  49. type ExtractVariables<T> = T extends { variables: object }
  50. ? T["variables"]
  51. : never;
  52. interface PageByUrlKeyResponse {
  53. pageByUrlKeypages?: PageData[];
  54. }
  55. const STOREFRONT_KEY = process.env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY;
  56. async function getBaseHeader() {
  57. /**
  58. * Headers:
  59. * X-LOCALE — locale code, e.g. "en", "fr", "ar"
  60. * X-CHANNEL — channel code, e.g. "default"
  61. * X-CURRENCY — currency code, e.g. "USD", "EUR", "INR"
  62. * */
  63. if(!STOREFRONT_KEY) {
  64. throw new Error("STOREFRONT_KEY must be configed!");
  65. }
  66. const cookieStore = await cookies();
  67. const xCurrency = cookieStore.get(CURRENT_CURRENCY)?.value; // 在proxy.ts中已经设置过值了
  68. const xLocal = cookieStore.get(CURRENT_LOCAL)?.value; // 在proxy.ts中已经设置过值了
  69. const xChannel = cookieStore.get(CURRENT_CHANNEL)?.value; // 在proxy.ts中已经设置过值了
  70. const baseHeaders: Record<string, string> = {
  71. "Content-Type": "application/json",
  72. "X-STOREFRONT-KEY": STOREFRONT_KEY,
  73. };
  74. if(xCurrency) {
  75. baseHeaders["X-Currency"] = xCurrency;
  76. }
  77. if(xLocal) {
  78. baseHeaders["X-Local"] = xLocal;
  79. }
  80. if(xChannel) {
  81. baseHeaders["X-Channel"] = xChannel;
  82. }
  83. return baseHeaders;
  84. }
  85. // rest api fetch
  86. export async function restApiFetch<T>({
  87. api,
  88. method,
  89. cache = "force-cache",
  90. headers,
  91. tags,
  92. variables,
  93. isCookies = true,
  94. guestToken,
  95. revalidate = 60,
  96. }: {
  97. api: string;
  98. method: "POST" | "GET" | "PUT" | "DELETE";
  99. cache?: RequestCache;
  100. headers?: HeadersInit | Record<string, string>;
  101. tags?: string[];
  102. variables?: ExtractVariables<T>;
  103. isCookies?: boolean;
  104. guestToken?: string;
  105. revalidate?: number;
  106. }): Promise<{ status: number; body: T } | never> {
  107. try {
  108. const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
  109. let accessToken: string | undefined = undefined;
  110. if (isCookies) {
  111. const sessions = (await getServerSession(
  112. authOptions,
  113. )) as BagistoSession | null;
  114. accessToken = sessions?.user?.accessToken;
  115. }
  116. const headerRes = await getBaseHeader();
  117. const baseHeaders: Record<string, string> = {...headerRes};
  118. if (accessToken) {
  119. baseHeaders.Authorization = `Bearer ${accessToken}`;
  120. } else if (guestToken) {
  121. baseHeaders.Authorization = `Bearer ${guestToken}`;
  122. }
  123. if (isCookies && headers) {
  124. if (headers instanceof Headers) {
  125. headers.forEach((value, key) => (baseHeaders[key] = value));
  126. } else {
  127. Object.assign(baseHeaders, headers);
  128. }
  129. }
  130. const param: RequestInit = {
  131. method: method,
  132. headers: baseHeaders,
  133. cache,
  134. next: {
  135. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  136. ...(tags && { tags }),
  137. },
  138. };
  139. if(variables) {
  140. param.body = JSON.stringify({...variables});
  141. }
  142. console.log('restApiFetch --- baseHeaders:', baseHeaders,apiUrl, param)
  143. const result = await fetch(apiUrl, param);
  144. console.log('restApiFetch --- result:', result)
  145. const body = await result.json();
  146. if (body.errors) throw body.errors[0];
  147. return { status: result.status, body };
  148. } catch (e) {
  149. throw e;
  150. }
  151. }
  152. // graphql fetch use for server side
  153. export async function serverGraphqlFetch<
  154. TData,
  155. TVariables = Record<string, never>
  156. >({
  157. cache = "no-store",
  158. headers,
  159. query,
  160. tags,
  161. variables,
  162. revalidate = 0,
  163. operationName = ''
  164. }: {
  165. cache?: RequestCache;
  166. headers?: HeadersInit | Record<string, string>;
  167. query: string | DocumentNode;
  168. tags?: string[];
  169. variables?: TVariables;
  170. revalidate?: number;
  171. operationName?: string;
  172. }): Promise<FetchGraphqlResult<TData>> {
  173. try {
  174. const queryString = typeof query === "string" ? query : print(query);
  175. let accessToken: string | undefined = undefined;
  176. let guestToken: string | undefined = undefined;
  177. const cookieStore = await cookies();
  178. const isGuest = cookieStore.get(IS_GUEST)?.value;
  179. const gt = cookieStore.get(GUEST_CART_TOKEN)?.value;
  180. if(isGuest === 'false') {
  181. // 登录用户
  182. const sessions = (await getServerSession(
  183. authOptions,
  184. )) as BagistoSession | null;
  185. accessToken = sessions?.user?.accessToken;
  186. } else if(gt) {
  187. // 游客
  188. const jwtRes = decodeJWT<{
  189. sessionToken: string;
  190. cartId: number;
  191. isGuest: boolean;
  192. }>(gt, true);
  193. guestToken = jwtRes?.sessionToken || '';
  194. }
  195. const headerRes = await getBaseHeader();
  196. const baseHeaders: Record<string, string> = {...headerRes};
  197. if (accessToken) {
  198. baseHeaders['Authorization'] = `Bearer ${accessToken}`;
  199. } else if (guestToken) {
  200. baseHeaders['Authorization'] = `Bearer ${guestToken}`;
  201. }
  202. if (headers) {
  203. if (headers instanceof Headers) {
  204. headers.forEach((value, key) => (baseHeaders[key] = value));
  205. } else {
  206. Object.assign(baseHeaders, headers);
  207. }
  208. }
  209. console.log('serverGraphqlFetch --- baseHeaders:', baseHeaders);
  210. const result = await fetch(GRAPHQL_URL, {
  211. method: "POST",
  212. headers: baseHeaders,
  213. body: JSON.stringify({
  214. query: queryString,
  215. ...(variables && { variables }),
  216. }),
  217. cache,
  218. next: {
  219. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  220. ...(tags && { tags }),
  221. },
  222. });
  223. const body = await result.json();
  224. console.log('serverGraphqlFetch --- body:',body);
  225. if (body.errors) {
  226. if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
  227. return { status: result.status, data: body.data, error: null };
  228. } else {
  229. return { status: result.status, data: body.data, error: body.errors[0] };
  230. }
  231. }
  232. return { status: result.status, data: body.data,error: null };
  233. } catch (e) {
  234. throw e;
  235. }
  236. }
  237. // graphql fetch use for route handler
  238. export async function bagistoFetch<T>({
  239. cache = "force-cache",
  240. headers,
  241. query,
  242. tags,
  243. variables,
  244. isCookies = true,
  245. guestToken,
  246. revalidate = 60,
  247. operationName = ''
  248. }: {
  249. cache?: RequestCache;
  250. headers?: HeadersInit | Record<string, string>;
  251. query: string | DocumentNode;
  252. tags?: string[];
  253. variables?: ExtractVariables<T>;
  254. isCookies?: boolean;
  255. guestToken?: string;
  256. revalidate?: number;
  257. operationName?: string;
  258. }): Promise<{ status: number; body: T } | never> {
  259. try {
  260. const queryString =
  261. typeof query === "string" ? query : print(query);
  262. let accessToken: string | undefined = undefined;
  263. if (isCookies) {
  264. const sessions = (await getServerSession(
  265. authOptions,
  266. )) as BagistoSession | null;
  267. accessToken = sessions?.user?.accessToken;
  268. }
  269. const headerRes = await getBaseHeader();
  270. const baseHeaders: Record<string, string> = {...headerRes};
  271. if (accessToken) {
  272. baseHeaders.Authorization = `Bearer ${accessToken}`;
  273. } else if (guestToken) {
  274. baseHeaders.Authorization = `Bearer ${guestToken}`;
  275. }
  276. if (isCookies && headers) {
  277. if (headers instanceof Headers) {
  278. headers.forEach((value, key) => (baseHeaders[key] = value));
  279. } else {
  280. Object.assign(baseHeaders, headers);
  281. }
  282. }
  283. // let cc = await cookies();
  284. // console.log('bagistoFetch --- url:', GRAPHQL_URL,isCookies);
  285. // console.log('bagistoFetch --- queryString:', queryString);
  286. console.log('bagistoFetch --- baseHeaders:', baseHeaders);
  287. // console.log('bagistoFetch --- cookies:', cc.getAll());
  288. // console.log('bagistoFetch --- variables:', variables);
  289. const result = await fetch(GRAPHQL_URL, {
  290. method: "POST",
  291. headers: baseHeaders,
  292. body: JSON.stringify({
  293. query: queryString,
  294. ...(variables && { variables }),
  295. }),
  296. cache,
  297. next: {
  298. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  299. ...(tags && { tags }),
  300. },
  301. });
  302. const body = await result.json();
  303. console.log('bagistoFetch --- body:',body);
  304. if (body.errors) {
  305. if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
  306. return { status: result.status, body };
  307. }
  308. throw body.errors[0]
  309. }
  310. return { status: result.status, body };
  311. } catch (e) {
  312. throw e;
  313. }
  314. }
  315. /*
  316. export async function bagistoFetchNoSession<T>({
  317. query,
  318. tags,
  319. variables,
  320. headers,
  321. cache = "force-cache",
  322. revalidate = 60,
  323. }: {
  324. query: string;
  325. tags?: string[];
  326. variables?: ExtractVariables<T>;
  327. headers?: HeadersInit | Record<string, string>;
  328. cache?: RequestCache;
  329. isCookies?: boolean;
  330. revalidate?: number;
  331. }): Promise<{ status: number; body: T } | never> {
  332. try {
  333. const result = await fetch(GRAPHQL_URL, {
  334. method: "POST",
  335. headers: {
  336. "Content-Type": "application/json",
  337. "X-STOREFRONT-KEY": STOREFRONT_KEY,
  338. "x-locale": "en",
  339. "x-currency": "USD",
  340. ...headers,
  341. },
  342. body: JSON.stringify({
  343. ...(query && { query }),
  344. ...(variables && { variables }),
  345. }),
  346. cache,
  347. next: {
  348. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  349. ...(tags && { tags }),
  350. },
  351. });
  352. const body = await result.json();
  353. if (body.errors) {
  354. throw body.errors[0];
  355. }
  356. return {
  357. status: result.status,
  358. body,
  359. };
  360. } catch (e) {
  361. throw { error: e, query };
  362. }
  363. }
  364. */
  365. export const removeEdgesAndNodes = <T>(array: Array<T>) => {
  366. return array?.map((edge) => edge);
  367. };
  368. const reshapeImages = (images: Array<ImageInfo>, productTitle: string) => {
  369. const flattened = removeEdgesAndNodes(images);
  370. return flattened.map((image) => {
  371. const filename = image?.url.match(/.*\/(.*)\..*/)?.[1];
  372. return {
  373. ...image,
  374. altText: image?.altText || `${productTitle} - ${filename}`,
  375. };
  376. });
  377. };
  378. const reshapeProduct = (
  379. product: BagistoProductInfo,
  380. filterHiddenProducts: boolean = true,
  381. ) => {
  382. if (
  383. !product ||
  384. (filterHiddenProducts && product.tags?.includes(HIDDEN_PRODUCT_TAG))
  385. ) {
  386. return undefined;
  387. }
  388. const { images, variants, ...rest } = product;
  389. return {
  390. ...rest,
  391. images: reshapeImages(images, product.title),
  392. variants: removeEdgesAndNodes(variants),
  393. };
  394. };
  395. export const reshapeProducts = (products: BagistoProductInfo[]) => {
  396. const reshapedProducts = [];
  397. for (const product of products) {
  398. if (product) {
  399. const reshapedProduct = reshapeProduct(product);
  400. if (reshapedProduct) {
  401. reshapedProducts.push(reshapedProduct);
  402. }
  403. }
  404. }
  405. return reshapedProducts;
  406. };
  407. export async function createUserToLogin(
  408. input: RegisterInputs,
  409. ): Promise<BagistoUser> {
  410. try {
  411. const { passwordConfirmation, ...userInput } = input;
  412. const res = await bagistoFetch<BagistoCreateUserOperation>({
  413. query: CUSTOMER_REGISTRATION,
  414. variables: {
  415. input: {
  416. ...userInput,
  417. confirmPassword: passwordConfirmation,
  418. status: "1",
  419. isVerified: "1",
  420. isSuspended: "0",
  421. subscribedToNewsLetter: true,
  422. },
  423. },
  424. cache: "no-store",
  425. revalidate: 3600,
  426. });
  427. return res.body.data.createCustomer.customer;
  428. } catch (error: any) {
  429. throw new Error(error?.message || "Registration failed");
  430. }
  431. }
  432. export async function logoutUser() {
  433. try {
  434. const session = await getServerSession(authOptions);
  435. const token = session?.user?.accessToken;
  436. if (!token) {
  437. return {
  438. success: false,
  439. message: "User token missing",
  440. };
  441. }
  442. /**
  443. * @todo CUSTOMER_LOGOUT 接口会报错,待修复(确定是php后端接口报错)
  444. */
  445. const res = await bagistoFetch<{
  446. data: { createLogout: { logout: { success: boolean; message: string } } };
  447. variables: { input: { token: string } };
  448. }>({
  449. query: CUSTOMER_LOGOUT,
  450. isCookies: true,
  451. revalidate: 3600,
  452. });
  453. const success = res?.body?.data?.createLogout?.logout?.success ?? false;
  454. const message =
  455. res?.body?.data?.createLogout?.logout?.message ?? "Logout executed";
  456. return {
  457. success,
  458. message,
  459. };
  460. } catch (error: unknown) {
  461. return {
  462. success: false,
  463. message: error instanceof Error ? error.message : "Something went wrong",
  464. };
  465. }
  466. }
  467. export async function recoverUserLogin(
  468. input: Record<string, unknown>,
  469. ): Promise<unknown> {
  470. try {
  471. return await bagistoFetch<{
  472. data: unknown;
  473. variables: Record<string, unknown>;
  474. }>({
  475. query: FORGET_PASSWORD,
  476. variables: {
  477. ...input,
  478. },
  479. cache: "no-store",
  480. revalidate: 3600,
  481. });
  482. } catch (error) {
  483. return error;
  484. }
  485. }
  486. export async function subscribeUser(
  487. input: Record<string, unknown>,
  488. ): Promise<unknown> {
  489. try {
  490. return await bagistoFetch<{
  491. data: unknown;
  492. variables: Record<string, unknown>;
  493. }>({
  494. query: SUBSCRIBE_TO_NEWSLETTER,
  495. variables: {
  496. ...input,
  497. },
  498. cache: "no-store",
  499. });
  500. } catch (error) {
  501. return error;
  502. }
  503. }
  504. export async function getThemeCustomization(): Promise<ThemeCustomizationResult> {
  505. try {
  506. const [{data: footerRes}, {data:servicesRes}] = await Promise.all([
  507. cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
  508. type: "footer_links",
  509. }),
  510. cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
  511. type: "services_content",
  512. }),
  513. ]);
  514. return {
  515. footer_links: footerRes,
  516. services_content: servicesRes,
  517. };
  518. } catch (err) {
  519. console.error("ThemeCustomization Error:", err);
  520. }
  521. return {
  522. footer_links: null,
  523. services_content: null,
  524. };
  525. }
  526. export async function revalidate(req: NextRequest): Promise<NextResponse> {
  527. const collectionWebhooks = [
  528. "collections/create",
  529. "collections/delete",
  530. "collections/update",
  531. ];
  532. const productWebhooks = [
  533. "products/create",
  534. "products/delete",
  535. "products/update",
  536. ];
  537. const topic = (await headers()).get("x-bagisto-topic") || "unknown";
  538. const secret = req.nextUrl.searchParams.get("secret");
  539. const isCollectionUpdate = collectionWebhooks.includes(topic);
  540. const isProductUpdate = productWebhooks.includes(topic);
  541. if (!secret || secret !== process.env.BAGISTO_REVALIDATION_SECRET) {
  542. return NextResponse.json({ status: 200 });
  543. }
  544. if (!isCollectionUpdate && !isProductUpdate) {
  545. return NextResponse.json({ status: 200, message: "No action needed" });
  546. }
  547. if (isProductUpdate) {
  548. revalidatePath("/", "layout");
  549. } else if (isCollectionUpdate) {
  550. revalidatePath("/", "layout");
  551. }
  552. return NextResponse.json({
  553. status: 200,
  554. revalidated: true,
  555. topic,
  556. now: Date.now(),
  557. });
  558. }
  559. export async function getHomePageData(): Promise<ThemeCustomizationResponse> {
  560. const res = await bagistoFetch<{
  561. data: ThemeCustomizationResponse;
  562. variables: { first: number };
  563. }>({
  564. query: GET_THEME_CUSTOMIZATION,
  565. variables: { first: 20 },
  566. tags: ["theme-customization"],
  567. revalidate: 60,
  568. });
  569. return res.body.data;
  570. }
  571. export async function getPage(input: { urlKey: string }): Promise<PageData[]> {
  572. const res = await bagistoFetch<{
  573. data: PageByUrlKeyResponse;
  574. variables: { pageByUrlKey: string };
  575. }>({
  576. query: PAGE_BY_URL_KEY,
  577. cache: "no-store",
  578. isCookies: false,
  579. variables: { pageByUrlKey: input.urlKey },
  580. });
  581. return res.body.data?.pageByUrlKeypages || [];
  582. }