WooGraphQL과 헤드리스 숍 구축: 제 4 장 5

banner

맨 위: 이 튜토리얼은 이전 WooGraphQL 릴리스를 다루고 더 이상 날짜가 없습니다. rewritten version은 진행 중입니다. 이 버전은 여기에서 공개됩니다.

https://www.youtube.com/embed/nhlexP-KtSo

본문 바로가기

  1. Headless eCommerce 응용 프로그램을위한 WordPress 설정
  2. eCommerce 앱 만들기 및 설정
  3. 빌딩 숍 목록 페이지 WooGraphQL
  4. 사용자 로그인 및 탐색
  5. 제품 페이지 및 카트 옵션 만들기

우리의 튜토리얼 시리즈의 다음 설치에 오신 것을 환영합니다, 우리는 사용자 탐색 및 로그인 페이지를 통합하여 전자 상거래 매장을 풍부하게합니다. 이 장은 사용자 인터페이스와 상호 작용 능력을 강화하는 데 관한 모든 것입니다. 더 많은 참여와 원활한 사용자 경험을 만들 수 있습니다.

사용자 로그인 및 탐색

eCommerce 사이트의 핵심 기능 중 하나는 사용자가 사이트의 다른 섹션을 탐색하고 계정에 로그인하고 관리 할 수있는 능력을 가지고하는 기능입니다. 상단 네비게이션 바는 네비게이션을 위한 중앙 허브 역할을 하며, 사용자가 사이트의 다양한 부품에 쉽게 접근할 수 있습니다. 상단 내비게이션 바에 사용자 내비게이션을 추가하면 유용성을 향상시키고 사용자들이 계정과 카트 세부 정보를 쉽게 접근할 수 있도록 도와줍니다.

또한, eCommerce 저장소에 로그인 페이지를 추가하는 과정을 통해 걸어갈 것입니다. 또한 디자인 된 로그인 페이지는 사용자 경험을 개인화하는 게이트웨이로서 온라인 상점에 중요하며, 사용자가 주문을 추적하고 좋아하는 제품을 저장하고 체크 아웃 프로세스를 가속화 할 수 있습니다.

거래를 더 감당하기 위해 우리는 시간과 노력을 절약 할 수있는 깔끔한 트릭을 탐험 할 것입니다. Next.js 애플리케이션에서 WooCommerce 저장소를 호스팅하는 WordPress 설치에 최종 사용자 세션을 통과하는 세션 드롭오프 링크를 사용하는 방법을 보여줍니다. 이 방법의 이점은 우리가 우리의 신청에 있는 특정 페이지를 창조하는 것을 우회할 것을 허용한다는 것입니다, 특히 우리의 정면 신청에서 즉시 유효하지 않는 기능에 건축하거나 그 의존할 수 있는 수시로 소문일 수 있었습니다.

자주 묻는 질문

시작하기 전에 업데이트해야 합니다. .env.local 이 장에 활력이 될 몇 가지 변수를 가진 파일.

SESSION_TOKEN_LS_KEY=[SESSION_TOKEN_LS_KEY]
REFRESH_TOKEN_LS_KEY=[REFRESH_TOKEN_LS_KEY]
AUTH_TOKEN_SS_KEY=[AUTH_TOKEN_SS_KEY]
AUTH_TOKEN_EXPIRY_SS_KEY=[AUTH_TOKEN_EXPIRY_SS_KEY]
CLIENT_CREDENTIALS_LS_KEY=[CLIENT_CREDENTIALS_LS_KEY]
CLIENT_SESSION_SS_KEY=[CLIENT_SESSION_SS_KEY]
CLIENT_SESSION_EXP_SS_KEY=[CLIENT_SESSION_EXP_SS_KEY]
NONCE_KEY=[NONCE_KEY]
NONCE_SALT=[NONCE_SALT]

값은 예외를 제외하고 임의일 수 있습니다. NONCE_KEY · NONCE_SALT WordPress에 해당되는 것과 일치해야합니다. 당신은 일반적으로이 값을 찾을 수 있습니다. WordPress 설치의 확인 wp-config.php 파일. 자주 묻는 질문 next.config.js 기타

...

const nextConfig = {
  ...
  
  },
  env: {
    ...
    SESSION_TOKEN_LS_KEY: process.env.SESSION_TOKEN_LS_KEY,
    REFRESH_TOKEN_LS_KEY: process.env.REFRESH_TOKEN_LS_KEY,
    AUTH_TOKEN_SS_KEY: process.env.AUTH_TOKEN_SS_KEY,
    AUTH_TOKEN_EXP_SS_KEY: process.env.AUTH_TOKEN_EXP_SS_KEY,
    CLIENT_CREDENTIALS_LS_KEY: process.env.CLIENT_CREDENTIALS_LS_KEY,
    CLIENT_SESSION_SS_KEY: process.env.CLIENT_SESSION_SS_KEY,
    CLIENT_SESSION_EXP_SS_KEY: process.env.CLIENT_SESSION_EXP_SS_KEY,
    NONCE_KEY: process.env.NONCE_KEY,
    NONCE_SALT: process.env.NONCE_SALT,
  },
}
...

이 튜토리얼의 끝으로, 사용자 탐색을 개선하기 위해 지식이 장착되어있어 사용자 친화적 인 로그인 페이지를 추가하고 세션 드롭오프 링크를 구현하여 전자 상거래 매장을 사용자 상호 작용과 기능의 다음 수준으로 가져갈 수 있습니다. 다이빙을 시작하자!

Part 1 로그인 페이지 생성 및 UserNav

1. 명세 설치하기 zod· react-hook-form· @hookform/resolvers 포장:

npm install zod react-hook-form @hookform/resolvers

이 패키지를 사용하여 로그인 양식을 관리하고 검증합니다.

2. 명세 이름 * /app/login/page.tsx::

// 앱/로그인/page.tsx
import { Login } from '@/client/Login';

export default function LoginPage() {
  return (
    <div className="min-h-screen">
      <Login/>
    </div>
  );
}

충분히. 계속하기

3. 명세 이름 * /client/Login::

// 클라이언트/로그인/로그인.tsx
import { useEffect } from 'react';
import * as z from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';

import { useSession } from '@/client/SessionProvider';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/ui/form';
import { Input } from '@/ui/input';
import { Button } from '@/ui/button';
import { LoadingSpinner } from '@/ui/LoadingSpinner';

export const LoginSchema = z.object({
  username: z.string().min(4, {
    message: 'Username must be at least 4 characters long',
  }),
  password: z.string().min(1, {
    message: 'Password must enter a password',
  })
});

export function Login() {
  const { login, isAuthenticated, fetching } = useSession();
  const router = useRouter();
  const form = useForm<z.infer<typeof LoginSchema>>({
    resolver: zodResolver(LoginSchema),
    defaultValues: {
      username: '',
      password: '',
    },
  });

  useEffect(() => {
    if (isAuthenticated) {
      router.push('/');
    }
  }, [isAuthenticated]);

  const onSubmit = (data: z.infer<typeof LoginSchema>) => {
    login(data.username, data.password);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-screen-lg mx-auto px-4">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="Enter your username or e-mail associate with your account." {...field}/>
              </FormControl>
              <FormMessage/>
            </FormItem>
          )}
/>
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" placeholder="Enter your password." {...field}/>
              </FormControl>
              <FormMessage/>
            </FormItem>
          )}
/>
        <Button
          type="submit"
          disabled={fetching}
          className="flex gap-x-2 items-center"
>
          Submit
          {fetching && <LoadingSpinner noText/>}
        </Button>
      </form>
    </Form>
  );
}

당신이 익숙한 경우 react-hook-form 이것은 당신에 대 한 이상 보이지 않습니다. 아직 전화하고 싶은 부분

const { login, isAuthenticated, fetching } = useSession();

useSession 이다 a 후크 에 SessionProvider 우리는 아직 구현되지 않았기 때문에 이것을 즉시 작동하지 않을 것입니다. 우리는 곧 도착합니다.

4. 명세 이름 * /client/UserNav::

import { useRouter } from 'next/navigation';

import { cn } from '@/utils/ui';
import { deleteClientSessionId } from '@woographql/utils/client';
import { useSession } from '@/client/SessionProvider';
import { NavLink, linkClassName } from '@/ui/NavLink';
import { Button } from '@/ui/button';

export function UserNav() {
  const { push } = useRouter();
  const {
    cart,
    customer,
    cartUrl,
    checkoutUrl,
    accountUrl,
    logout: killSession,
    isAuthenticated,
    fetching,
  } = useSession();
  
  const goToCartPage = () => {
    deleteClientSessionId();
    console.log('Go to cart page');
    window.location.href = cartUrl;
  };
  const goToCheckoutPage = () => {
    deleteClientSessionId();
    window.location.href = checkoutUrl;
  };
  const goToAccountPage = () => {
    deleteClientSessionId();
    window.location.href = accountUrl;
  };

  const logout = () => {
    killSession(`Goodbye, ${customer?.firstName}`);
  };

  return (
    <>
      <li className="group">
        <Button
          className={cn(
            'flex flex-row gap-x-2 items-center p-0 hover:no-underline text-base font-normal',
            linkClassName,
          )}
          disabled={fetching}
          variant='link'
          onClick={goToCartPage}
>
          <span>{cart?.contents?.itemCount || 0}</span>
          Cart
        </Button>
      </li>
      <li className="group w-auto">
        <Button
          className={cn(
            'p-0 hover:no-underline text-base font-normal',
            linkClassName,
          )}
          disabled={fetching}
          variant='link'
          onClick={goToCheckoutPage}
>
          Checkout
        </Button>
      </li>
      {isAuthenticated ? (
        <>
          <li className="group">
            <Button
              className={cn(
                'p-0 hover:no-underline text-base font-normal',
                linkClassName,
              )}
              disabled={fetching}
              variant='link'
              onClick={goToAccountPage}
>
              Account
            </Button>
          </li>
          <li className="group">
            <Button
              className={cn(
                'p-0 hover:no-underline text-base font-normal',
                linkClassName,
              )}
              disabled={fetching}
              variant='link'
              onClick={logout}
>
              Logout
            </Button>
          </li>
        </>
      ) : (
        <li className="group">
          <NavLink href="/login">
            Login
          </NavLink>
        </li>
      )}
    </>
  )
}

더 보기 UserNav 구성 요소는 주어진 값에 따라 표시 사용자 행동에 책임이 있습니다. SessionProvider·

const {
    cart, // Cart data object.
    customer, // Customer data object.
    goToCartPage, // Callback for sending the user to the Cart Page.
    goToAccountPage, // Callback for sending the user to the Account Page.
    goToCheckoutPage, // Callback for sending the user to the Checkout Page.
    logout,  // Callback for deleting end-user's session.
    isAuthenticated, // Flag determining end-user's login status
    refetchUrls, // Callback to begin process of generating new session drop-off urls.
    fetching, // Flag determining session handler fetcher status.
} = useSession();

이것에 추가하자 /server/TopNav 마지막으로 움직이기 전에 구성 요소 SessionProvider·

5. 명세 (주) /server/TopNav/TopNav.tsx::

// 서버/TopNav/TopNav.tsx
...

import { UserNav } from '@/client/UserNav';
...

export function TopNav({ menu }: TopNavProps) {
  return (
    <nav className="w-full bg-white min-h-24 py-4 px-4">
      <ul className="max-w-screen-lg m-auto w-full flex flex-row gap-x-4 justify-end items-center">
        ...
        <UserNav/>
      </ul>
    </nav>
  )
}

Part 2: SessionProvider 만들기

1. 명세 이름 * /client/SessionProvider::

// 클라이언트/서비스제공자/SessionProvider.tsx
import {
  createContext,
  PropsWithChildren,
  useContext,
  useReducer,
  useEffect,
} from 'react';

import {
  Customer,
  Cart,
} from '@/graphql';

import { useToast } from '@/ui/use-toast';

export interface SessionContext {
  isAuthenticated: boolean;
  hasCredentials: boolean;
  cart: Cart|null;
  customer: Customer|null;
  cartUrl: string;
  checkoutUrl: string;
  accountUrl: string;
  fetching: boolean;
  logout: (message?: string) => void;
  login: (username: string, password: string, successMessage?: string) => Promise<boolean>;
  refetch: () => Promise<boolean>;
}

const initialContext: SessionContext = {
  isAuthenticated: false,
  hasCredentials: false,
  cart: null,
  customer: null,
  cartUrl: '',
  checkoutUrl: '',
  accountUrl: '',
  fetching: false,
  logout: (message?: string) => null,
  login: (username: string, password: string) => new Promise((resolve) => { resolve(false); }),
  refetch: () => new Promise((resolve) => { resolve(false); }),
};

export const sessionContext = createContext<SessionContext>(initialContext);

type SessionAction = {
  type: 'UPDATE_STATE';
  payload: SessionContext;
} | {
  type: 'LOGOUT';
};

const reducer = (state: SessionContext, action: SessionAction): SessionContext => {
  switch (action.type) {
    case 'UPDATE_STATE':
      return {
        ...state,
        ...action.payload,
      };
    case 'LOGOUT':
      return {
        ...state,
        customer: null,
        cart: null,
      };
    default:
      throw new Error('Invalid action dispatched to session data reducer');
  }
};

const { Provider } = sessionContext;

export function SessionProvider({ children }: PropsWithChildren) {
  const { toast } = useToast();
  const [state, dispatch] = useReducer(reducer, initialContext);

  // 로그아웃 상태를 삭제하고 로컬 저장소를 구합니다.
  const logout = () => {
    dispatch({ type: 'LOGOUT' });
  };
      

  const store: SessionContext = {
    ...state,
    isAuthenticated: !!state.customer?.id && 'guest' !== state.customer.id,
    hasCredentials: false,
    logout,
  };
  return (
    <Provider value={store}>{children}</Provider>
  );
}

export const useSession = () => useContext(sessionContext);

이제 그것에 추가 /app/layout.tsx

2. 명세 (주) /app/layout.tsx::

// 앱/레이아웃.tsx
...

import { SessionProvider } from '@/client/SessionProvider';
...

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  ...

  return (
    <html lang="en">
      <body>
        <SessionProvider>
          <TopNav menu={menu}/>
          <main className="w-full">
            {children}
          </main>
        </SessionProvider>
        <Toaster/>
      </body>
    </html>
  )
}

이 응용 프로그램은 성공적으로 구축하고 실행되지만, 우리가 소개 한 구성 요소의 아무도 실제로 작동. keen 눈과 함께 우리는 우리의 endpoint와 통신 할 논리를 구현하지 못했습니다.

최종 사용자의 보안을 처리하기 때문에 세션 자격 증명 및 GraphQL 엔드포인트의 제한적 가시성의 정교한 관리가 필요합니다. 지금까지 생성 된 모든 것에도 불구하고하지 않아 작업이 GraphQL 엔드포인트를 노출시키는 라이브 응용 프로그램에 발생합니다. 믿지 말라. 브라우저 개발자 도구를 열고 확인 Network 탭. 페이지를 새로 고침하고, 무엇이든을 클릭,하지만 당신은 당신의 GraphQL 엔드 포인트에 볼 수 없습니다.

이제까지 모든 GraphQL 요청은 페이지 빌드 시간 동안 서버 레벨 만 모든 실행에 이루어졌지만, 클라이언트에서 직접 GraphQL 쿼리를 실행하면 엔드포인트를 노출하는 최종 사용자 세션을 처리하는 일부 클라이언트 측 요청을 만들 수 있습니다.

그래서 우리는 클라이언트에서 그들을 실행하지 않습니다 그러나 대신 다음 13 루트 페이지의 이점을 복용. GraphQL 요청을 클라이언트의 관점에서 실행하는 몇 가지 경로 페이지를 만들 것입니다. 우리는 논리를 정의 할 일부 유틸리티 파일을 만들려면이 경로 페이지와 통신하고 시간 감지 세션 자격 관리. 그래서 시작합시다.

Part 3: 유틸리티 파일을 만듭니다.

1. 명세 설치하기 crypto-js · jwt-decode 포장:

npm install crypto-js jwt-decode

이 라이브러리는 WordPress의 해싱 기능을 재현합니다.

2. 명세 이름 * /utils/nonce.ts::

// utils/nonce.ts의
import HmacMD5 from 'crypto-js/hmac-md5';
import jwtDecode from 'jwt-decode';

export const MINUTE_IN_SECONDS = 60;
export const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS;
export const DAY_IN_SECONDS = 24 * HOUR_IN_SECONDS;

export function time() {
  return Math.floor(new Date().getTime() / 1000);
}

export function nonceTick() {
  const nonceLife = DAY_IN_SECONDS;
  return Math.ceil(time() / (nonceLife / 2));
}

export function wpNonceHash(data: string) {
  const nonceSalt = process.env.NONCE_KEY as string + process.env.NONCE_SALT as string;
  const hash = HmacMD5(data, nonceSalt).toString();

  return hash;
}

export function createNonce(action: string, uId:string|number, token:string) {
  const i = nonceTick();

  const nonce = wpNonceHash(`${i}|${action}|${uId}|${token}`).slice(-12, -2);

  return nonce;
}

export enum ActionTypes {
  Cart = 'cart',
  Checkout = 'checkout',
  Account = 'account',
}

function getAction(action: ActionTypes, uId: string|number) {
  switch (action) {
    case ActionTypes.Cart:
      return `load-cart_${uId}`;
    case ActionTypes.Checkout:
      return `load-checkout_${uId}`;
    case ActionTypes.Account:
      return `load-account_${uId}`;
    default:
      throw new Error('Invalid nonce action provided.');
  }
}

function getNonceParam(action: ActionTypes) {
  switch (action) {
    case ActionTypes.Cart:
      return '_wc_cart';
    case ActionTypes.Checkout:
      return '_wc_checkout';
    case ActionTypes.Account:
      return '_wc_account';
    default:
      throw new Error('Invalid nonce action provided.');
  }
}

type DecodedToken = {
  data: { customer_id: string };
}
export function getUidFromToken(sessionToken: string) {
  const decodedToken = jwtDecode<DecodedToken>(sessionToken);
  if (!decodedToken?.data?.customer_id) {
    throw new Error('Failed to decode session token');
  }
  return decodedToken.data.customer_id;
}

export function generateUrl(sessionToken:string, clientSessionId:string, actionType: ActionTypes) {
  const uId = getUidFromToken(sessionToken);
  const action = getAction(actionType, uId);

  // nonce 만들기
  const nonce = createNonce(action, uId, clientSessionId);

  // URL 만들기.
  const param = getNonceParam(actionType);
  let url = `${process.env.BACKEND_URL}/wp/transfer-session?session_id=${uId}&${param}=${nonce}`;

  return url;
}

나는이 파일에 대한 많은 세부 사항으로 갈 것이다, 그러나 그것은 주로 몇 PHP / WP 기능 / 일관성의 JS 클론입니다. 우리의 경우 session 우리가 걱정하는 유틸리티 time 기능 및 MINUTE_IN_SECONDS 기타 제품 계속하기

3. 명세 이름 * /utils/client.ts::

// utils/클라이언트.ts
import {
  wpNonceHash,
  time,
  HOUR_IN_SECONDS,
  MINUTE_IN_SECONDS,
  DAY_IN_SECONDS,
} from '@/utils/nonce';

type Creds = {
  userAgent: string;
  ip: string;
  issued: number;
}

async function createClientSessionId() {
  const encodedCredentials = localStorage.getItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string);
  let credentials: null|Creds = encodedCredentials ? JSON.parse(encodedCredentials) : null;
  if (!credentials || time()> credentials.issued + (14 * DAY_IN_SECONDS)) {
    // Create credentials object with UserAgent.
    credentials = {
      userAgent: window?.navigator?.userAgent || '',
      ip: '',
      issued: 0,
    };

    // 파일 형식
    const response = await fetch('https://api.ipify.org/?format=json');
    const { data } = await response.json();
    credentials.ip = data?.ip || '';
  }

  // new nonces를 보장하기 위해 timestamp를 업데이트합니다.
  // 최종 사용자는 그 응용 프로그램을 시작합니다.
  credentials.issued = time();
  localStorage.setItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string, JSON.stringify(credentials));

  // Client Session ID 생성
  const clientSessionId = wpNonceHash(JSON.stringify(credentials));
  const timeout = `${credentials.issued + HOUR_IN_SECONDS}`;

  // 클라이언트 세션 ID 저장.
  sessionStorage.setItem(process.env.CLIENT_SESSION_SS_KEY as string, clientSessionId);
  sessionStorage.setItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string, timeout);

  // 클라이언트 세션 ID를 반환합니다.
  return { clientSessionId, timeout };
}

function hasSessionToken() {
  const sessionToken = localStorage.getItem(process.env.SESSION_TOKEN_LS_KEY as string);

  return !!sessionToken;
}

let clientSetter: ReturnType<typeof setInterval>;

/**
 * Creates timed fetcher for renewing client credentials and client session id.
 * 
 * @returns {void}
 */
function setClientFetcher() {
  if (clientSetter) {
    clearInterval(clientSetter);
  }
  clientSetter = setInterval(
    async () => {
      if (!hasSessionToken()) {
        clearInterval(clientSetter);
        return;
      }
      createClientSessionId();
    },
    Number(45 * MINUTE_IN_SECONDS),
  );
}

export async function getClientSessionId() {
  let clientSessionId = sessionStorage.getItem(process.env.CLIENT_SESSION_SS_KEY as string);
  let timeout = sessionStorage.getItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string);
  if (!clientSessionId || !timeout || time()> Number(timeout)) {
    ({ clientSessionId, timeout } = await createClientSessionId());
    setClientFetcher();
  }

  return { clientSessionId, timeout };
}

export function deleteClientSessionId() {
  if (clientSetter) {
    clearInterval(clientSetter);
  }
  sessionStorage.removeItem(process.env.CLIENT_SESSION_SS_KEY as string);
  sessionStorage.removeItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string);
}

export function deleteClientCredentials() {
  deleteClientSessionId();
  localStorage.removeItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string);
}

여기에서 논리는 창조를 위한 것입니다 clientSessionId. 이것은 최종 사용자의 기계에 세션 드롭오프 링크 사용을 제한하는 데 필요합니다. 우리는 노선 페이지를 만들기 위해 더 많은 것을 얻을 것이다. 현재에 대한 한 가지는 시간 민감하고 논리의 대부분이 평가 및 갱신에 전념한다는 것입니다.

4. 명세 이름 * /utils/session.ts::

import { GraphQLError } from 'graphql/error';

import {
  Customer,
  Cart,
} from '@/graphql';
import { getClientSessionId } from '@/utils/client';
import { MINUTE_IN_SECONDS, time } from '@/utils/nonce';

type ResponseErrors = {
  errors?: {
    message: string;
    data?: unknown;
  }
}
async function apiCall<T>(url: string, input: globalThis.RequestInit) {
  const response = await fetch(
    url,
    {
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
      ...input,
    },
  );

  const json: T&ResponseErrors = await response.json();

  // 캡처 오류.
  if (json?.errors || response.status !== 200) {
    throw new Error(json.errors?.message || `Failed to fetch: ${url}`);
  }

  return json;
}

// Auth 관리.
function saveCredentials(authToken: string, sessionToken?: string, refreshToken?: string) {
  sessionStorage.setItem(process.env.AUTH_TOKEN_SS_KEY as string, authToken);
  if (!!sessionToken) {
    localStorage.setItem(process.env.SESSION_TOKEN_LS_KEY as string, sessionToken);
  }
  if (refreshToken) {
    localStorage.setItem(process.env.REFRESH_TOKEN_LS_KEY as string, refreshToken);
  }
}

function saveSessionToken(sessionToken: string) {
  localStorage.setItem(process.env.SESSION_TOKEN_LS_KEY as string, sessionToken);
}

export function hasCredentials() {
  const sessionToken = localStorage.getItem(process.env.SESSION_TOKEN_LS_KEY as string);
  const authToken = sessionStorage.getItem(process.env.AUTH_TOKEN_SS_KEY as string);
  const refreshToken = localStorage.getItem(process.env.REFRESH_TOKEN_LS_KEY as string);

  if (!!sessionToken && !!authToken && !!refreshToken) {
    return true;
  }

  return false;
}

function setAuthTokenExpiry() {
  const authTimeout = time() + (15 * MINUTE_IN_SECONDS);
  sessionStorage.setItem(process.env.AUTH_TOKEN_EXPIRY_SS_KEY as string, `${authTimeout}`);
}

function authTokenIsExpired() {
  const authTimeout = sessionStorage.getItem(process.env.AUTH_TOKEN_EXPIRY_SS_KEY as string);
  if (!authTimeout || Number(authTimeout) < time()) {
    return true;
  }
}

type FetchAuthTokenResponse = {
  authToken: string;
  sessionToken: string;
}

async function fetchAuthToken() {
  const refreshToken = localStorage.getItem(process.env.REFRESH_TOKEN_LS_KEY as string);
  if (!refreshToken) {
    // eslint-disable-next-line no-console의 경우
    isDev() && console.error('Unauthorized');
    return null;
  }

  const json = await apiCall<FetchAuthTokenResponse>(
    '/api/auth',
    {
      method: 'POST',
      body: JSON.stringify({ refreshToken }),
    },
  );

  const { authToken, sessionToken } = json;
  saveCredentials(authToken, sessionToken);
  setAuthTokenExpiry();

  return authToken;
}

let tokenSetter: ReturnType<typeof setInterval>;
function setAutoFetcher() {
  if (tokenSetter) {
    clearInterval(tokenSetter);
  }
  tokenSetter = setInterval(
    async () => {
      if (!hasCredentials()) {
        clearInterval(tokenSetter);
        return;
      }
      fetchAuthToken();
    },
    Number(process.env.AUTH_KEY_TIMEOUT || 30000),
  );
}

type LoginResponse = {
  authToken: string
  refreshToken: string;
  sessionToken: string;
}
export async function login(username: string, password: string): Promise<boolean|string> {
  let json: LoginResponse;
  try {
    json = await apiCall<LoginResponse>(
      '/api/login',
      {
        method: 'POST',
        body: JSON.stringify({ username, password }),
      },
    );
  } catch (error) {
    return (error as GraphQLError)?.message || error as string;
  }

  const { authToken, refreshToken, sessionToken } = json;
  saveCredentials(authToken, sessionToken, refreshToken);
  setAutoFetcher();

  return true;
}

export async function getAuthToken() {
  let authToken = sessionStorage.getItem(process.env.AUTH_TOKEN_SS_KEY as string);
  if (!authToken || authTokenIsExpired()) {
    authToken = await fetchAuthToken();
  }

  if (authToken && !tokenSetter) {
    setAutoFetcher();
  }
  return authToken;
}

type FetchSessionTokenResponse = {
  sessionToken: string;
}

async function fetchSessionToken() {
  const json = await apiCall<FetchSessionTokenResponse>(
    '/api/auth',
    { method: 'GET' },
  );

  const { sessionToken } = json;

  sessionToken && saveSessionToken(sessionToken);
  return sessionToken;
}

async function getSessionToken() {
  let sessionToken = localStorage.getItem(process.env.SESSION_TOKEN_LS_KEY as string);
  if (!sessionToken) {
    sessionToken = await fetchSessionToken();
  }
  return sessionToken;
}

export function hasRefreshToken() {
  const refreshToken = localStorage.getItem(process.env.REFRESH_TOKEN_LS_KEY as string);

  return !!refreshToken;
}

export function hasAuthToken() {
  const authToken = sessionStorage.getItem(process.env.AUTH_TOKEN_SS_KEY as string);

  return !!authToken;
}

export type FetchSessionResponse = {
  customer: Customer;
  cart: Cart;
}
export async function getSession(): Promise<FetchSessionResponse|string> {
  const authToken = await getAuthToken();
  const sessionToken = await getSessionToken();
  let json: FetchSessionResponse;
  try {
    json = await apiCall<FetchSessionResponse>(
      '/api/session',
      {
        method: 'POST',
        body: JSON.stringify({
          sessionToken,
          authToken,
        }),
      },
    );
  } catch (error) {
    return (error as GraphQLError)?.message || error as string;
  }

  const { customer } = json;
  saveSessionToken(customer.sessionToken as string);

  return json;
}



export type FetchAuthURLResponse = {
  cartUrl: string;
  checkoutUrl: string;
  accountUrl: string
}
export async function fetchAuthURLs(): Promise<FetchAuthURLResponse|string> {
  const authToken = await getAuthToken();
  const sessionToken = await getSessionToken();
  const { clientSessionId, timeout } = await getClientSessionId();
  let json: FetchAuthURLResponse;
  try {
    json = await apiCall<FetchAuthURLResponse>(
      '/api/nonce',
      {
        method: 'POST',
        body: JSON.stringify({
          sessionToken,
          authToken,
          clientSessionId,
          timeout,
        }),
      },
    );
  } catch (error) {
    return (error as GraphQLError)?.message || error as string;
  }

  return json;
}

export function deleteCredentials() {
  if (tokenSetter) {
    clearInterval(tokenSetter);
  }
  localStorage.removeItem(process.env.SESSION_TOKEN_LS_KEY as string);
  sessionStorage.removeItem(process.env.AUTH_TOKEN_SS_KEY as string);
  localStorage.removeItem(process.env.REFRESH_TOKEN_LS_KEY as string);
}

조금 쇠고기이지만이 예쁜 직선입니다. login· getSession· fetchAuthURLs 콜백은 다음과 같이 사용됩니다. SessionProvider·

  • login: 요청을 보내면 /api/login 최종 사용자의 경로 username · password 입력, 성공하면 authToken · refreshToken 반환합니다.
  • getSession: end-user의 Session credentials에 대한 첫 번째 검색 localStorage 다음 POST 요청을 보냅니다. /api/session 최종 사용자의 authToken · sessionToken ID를 위해.
    • 최종 사용자의 경우 refreshToken 그리고 유효하지 않음 authToken 그리고 새로운 authToken POST 요청을 전송하여 검색 /api/auth 내 계정 fetchAuthToken·
    • 최종 사용자의 자격은 새로운 것 sessionToken GET 요청을 전송하여 검색 /api/auth 내 계정 fetchSessionToken
  • fetchAuthURLs: 정확히 같은 작품 getSession 그것을 제외하고 clientSessionId · clientSessionIdTimeout 또한 세션 드롭오프 URL 생성을 위해.
    • end-user가 유효하지 않은 경우 clientSessionId, 새로운 것은 생성됩니다.

만든 모든 유틸리티 파일로. 노선 페이지로 이동합시다.

Part 4: 루트 페이지 만들기

1. 명세 이름 * app/api/auth/route.ts::

// 앱/api/auth/route.ts
import { NextResponse } from 'next/server';
import { print } from 'graphql';

import {
  GetSessionDocument,
  GetSessionQuery,
  RefreshAuthTokenDocument,
  RefreshAuthTokenMutation,
  getClient,
} from '@/graphql';

export async function GET(request: Request) {
  try {
    const client = getClient();

    const { data, headers } = await client.rawRequest<GetSessionQuery>(
      print(GetSessionDocument),
    );

    const cart = data?.cart;
    const customer = data?.customer;
    const sessionToken = headers.get('woocommerce-session');

    if (!cart || !customer || !sessionToken) {
      return NextResponse.json({ errors: { message: 'Failed to retrieve session credentials.' } }, { status: 500 });
    }

    return NextResponse.json({ sessionToken });
  } catch (err) {
    console.log(err);
    return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
  }
}

export async function POST(request: Request) {
  try {
    const client = getClient();

    const body = await request.json();
    let authToken = body.authToken;
    const refreshToken = body.refreshToken;
    if (!authToken && !refreshToken) {
      return NextResponse.json({ errors: { message: 'No refresh token provided' } }, { status: 500 });
    }

    if (!authToken && refreshToken) {
      client.setHeaders({});
      const results = await client.request<RefreshAuthTokenMutation>(
        RefreshAuthTokenDocument,
        { refreshToken },
      );

      authToken = results?.refreshJwtAuthToken?.authToken;

      if (!authToken) {
        return NextResponse.json({ errors: { message: 'Failed to retrieve auth token.' } }, { status: 500 });
      }
    }

    client.setHeaders({ Authorization: `Bearer ${authToken}` });
    const { data: cartData, headers } = await client.rawRequest<GetSessionQuery>(
      print(GetSessionDocument),
    );

    const newSessionToken = cartData?.customer?.sessionToken;
    if (!newSessionToken) {
      return NextResponse.json({ errors: { message: 'Failed to validate auth token.' } }, { status: 500 });
    }

    const sessionToken = headers.get('woocommerce-session') || newSessionToken;

    return NextResponse.json({ authToken, sessionToken });
  } catch (err) {
    console.log(err);
    return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
  }
}

이 루트는 모두 있지만 GET · POST 콜백, 둘 다 다른 상황에서 같은 목표를 시도.

  • POST 사용자를 가정하는 것은 반환 사용자입니다. refreshToken 또는 authToken 성공하면 authToken · sessionToken·
  • GET 사용자를 가정하는 것은 credentials 없이 새로운 이고 새로운 반환 sessionToken 사용자를 위해.

2. 명세 이름 * /app/api/login/route.ts::

import { NextResponse } from 'next/server';
import { print } from 'graphql';

import {
  GetSessionDocument,
  GetSessionQuery,
  LoginDocument,
  LoginMutation,
  getClient,
} from '@/graphql';

type RequestBody = ({
  auth: string;
  username: undefined;
  password: undefined;
} | {
  auth: undefined;
  username: string;
  password: string;
})

export async function POST(request: Request) {
  try {
    const { username, password } = await request.json() as RequestBody;
    const graphQLClient = getClient();

    if (!username || !password) {
      return NextResponse.json({
        errors: {
          message: 'Proper credential must be provided for authentication',
        },
      }, { status: 500 });
    }

    const data = await graphQLClient.request<LoginMutation>(LoginDocument, { username, password });
    if (!data?.login) {
      return NextResponse.json({ errors: { message: 'Login failed.' } }, { status: 500 });
    }

    const { authToken, refreshToken } = data?.login;
    if (!authToken || !refreshToken) {
      return NextResponse.json({ errors: { message: 'Failed to retrieve credentials.' } }, { status: 500 });
    }

    graphQLClient.setHeader('Authorization', `Bearer ${data.login.authToken}`);
    const { data:_, headers } = await graphQLClient.rawRequest<GetSessionQuery>(print(GetSessionDocument));
    
    const sessionToken = headers.get('woocommerce-session');
    if (!sessionToken) {
      return NextResponse.json({ errors: { message: 'Failed to retrieve session token.' } }, { status: 500 });
    }


    return NextResponse.json({ authToken, refreshToken, sessionToken });
  } catch (err) {
    console.log(err);
    return NextResponse.json({ errors: { message: 'Login credentials invalid.' } }, { status: 500 });
  }
}

이것은 마지막 것 그러나 더 간단합니다. 그것은 걸립니다 username · password 입력 및 트리에서 최종 사용자의 반환 authToken· refreshToken· sessionToken·

참고, 그 sessionToken HTTP 응답에서 검색된다 GetSession 자주 묻는 질문 authToken 설정하기 Authorization. 이 이유는 데이터베이스의 사용자에 연결된 마지막 세션을 얻는 것입니다. 만약에 sessionToken 이름 * customer 이 새로운 세션 대신에 쿼리가 사용됩니다. login 매번 이전 세션은 두 번째로 새로운 시대가 될 것입니다. sessionToken 사용. 패턴도 사용되었습니다. POST 콜백의 api/auth 오시는 길

3. 명세 이름 * /app/api/session/route.ts::

import { NextResponse } from 'next/server';
import {
  GetSessionDocument,
  GetSessionQuery,
  Cart,
  Customer,
  getClient,
} from '@/graphql';

type RequestBody = {
  sessionToken: string;
  authToken?: string;
}

type GraphQLRequestHeaders = {
  'woocommerce-session': string;
  Authorization?: string;
}

export async function POST(request: Request) {
  try {
    const { sessionToken, authToken } = await request.json() as RequestBody;

    if (!sessionToken) {
      return NextResponse.json({ errors: { message: 'Session not started' } }, { status: 500 });
    }
    const headers: GraphQLRequestHeaders = { 'woocommerce-session': `Session ${sessionToken}` };
    if (authToken) {
      headers.Authorization = `Bearer ${authToken}`;
    }
    const graphQLClient = getClient();
    graphQLClient.setHeaders(headers);

    const results = await graphQLClient.request<GetSessionQuery>(
      GetSessionDocument,
    );

    const customer = results?.customer as Customer;
    const cart = results?.cart as Cart;

    if (!customer) {
      return NextResponse.json({ errors: { message: 'Failed to retrieve customer data.' } }, { status: 500 });
    }

    if (!cart) {
      return NextResponse.json({ errors: { message: 'Failed to retrieve cart data.' } }, { status: 500 });
    }

    return NextResponse.json({ customer, cart });
  } catch (err) {
    console.log(err);
    return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
  }
}

이 것은 sessionToken 그리고 아마도 authToken. 적절한 자격 증명을 부여하면 최종 사용자의 반환 customer · cart 데이터에서 GetSession 이름 *

4. 명세 이름 * /api/nonce/route.ts::

import { NextResponse } from 'next/server';

import { UpdateSessionDocument, UpdateSessionMutation, getClient } from '@/graphql';
import { ActionTypes, generateUrl } from '@/utils/nonce';

type RequestBody = {
  sessionToken: string;
  authToken?: string;
  clientSessionId: string;
  timeout: number;
}

type GraphQLRequestHeaders = {
  'woocommerce-session': string;
  Authorization?: string;
}

export async function POST(request: Request) {
  try {
    const {
      sessionToken,
      authToken,
      clientSessionId,
      timeout,
    } = await request.json() as RequestBody;

    if (!sessionToken) {
      return NextResponse.json({ errors: { message: 'No session started.' } }, { status: 500 });
    }

    if (!clientSessionId || !timeout) {
      return NextResponse.json({ errors: { message: 'Client Session ID and expiration must be provided.' } }, { status: 500 });
    }

    const headers: GraphQLRequestHeaders = { 'woocommerce-session': `Session ${sessionToken}` };
    if (authToken) {
      headers.Authorization = `Bearer ${authToken}`;
    }
    const client = getClient();
    client.setHeaders(headers);

    const input = {
      sessionData: [
        {
          key: 'client_session_id',
          value: clientSessionId,
        },
        {
          key: 'client_session_id_expiration',
          value: timeout,
        },
      ],
    };
    const results = await client.request<UpdateSessionMutation>(
      UpdateSessionDocument,
      { input },
    );

    if (!results.updateSession) {
      const message = 'Failed to update session';
      return NextResponse.json({ errors: { message } }, { status: 500 });
    }

    const cartUrl = generateUrl(sessionToken, clientSessionId, ActionTypes.Cart);
    const checkoutUrl = generateUrl(sessionToken, clientSessionId, ActionTypes.Checkout);
    const accountUrl = generateUrl(sessionToken, clientSessionId, ActionTypes.Account);

    return NextResponse.json({ cartUrl, checkoutUrl, accountUrl });
  } catch (err) {
    console.log(err);
    return NextResponse.json({ errors: { message: 'Sorry, something went wrong' } }, { status: 500 });
  }
}

이 루트는 최종 사용자와 동일한 입력을 기대합니다. clientSessionId · clientSessionIdTimeout. 적절한 입력을 주면 최종 사용자의 설정 client_session_id · client_session_id_expiration 서버의 end-user의 WooCommerce 세션 객체. 이 값은 세션 드롭오프 URL에서 사용되는 WP 백엔드의 일련의 정오를 생성하는 데 사용됩니다.

이제 우리는 GraphQL 엔드포인트의 전체 세션 드롭오프 URL을 쿼리 할 수 있지만, transit에 누출되는 nonces/URL의 위험을 실행합니다. 그래서 대신 우리는 기능의 나머지를 사용하여 정의 utils/nonce.ts 파일과 우리 루트 페이지의 정확한 동일한 nonces를 다시 작성합니다. generateUrl End-user에 대한 Session drop-off URL tailor-made를 생성하고 기계에 묶습니다. 이 마지막 경로 페이지가 생성되고 모든 왼쪽은 업데이트 SessionProvider 유틸리티 파일을 활용하고 경로 페이지를 확장하여.

부분 5: 끝 SessionProvider

1. 명세 (주) /client/SessionProvider/SessionProvider.tsx::

// 클라이언트/서비스제공자/SessionProvider.tsx
...

import {
  hasCredentials,
  deleteCredentials,
  getSession as getSessionApiCall,
  FetchSessionResponse as Session,
  FetchAuthURLResponse as AuthUrls,
  fetchAuthURLs as fetchAuthURLsApiCall,
  login as loginApiCall,
} from '@/utils/session';
import {
  deleteClientSessionId,
  deleteClientCredentials,
} from '@/utils/client';
import { useToast } from '@/ui/use-toast';
...

export function SessionProvider({ children }: PropsWithChildren) {
  ...

  // 로그아웃 상태를 삭제하고 로컬 저장소를 구합니다.
  const logout = () => {
    ...
    deleteCredentials();
    deleteClientCredentials();
    fetchSession();
  };

  // 프로세스 세션 fetch 요청 응답.
  const setSession = (session: Session|string, authUrls: AuthUrls|string) => {
    if (typeof session === 'string') {
      toast({
        title: 'Fetch Session Error',
        description: 'Failed to fetch session.',
        variant: 'destructive'
      });
    }
    if (typeof authUrls === 'string') {
      toast({
        title: 'Session Error',
        description: 'Failed to generate session URLs. Please refresh the page.',
        variant: 'destructive'
      });
    }

    if (typeof session === 'string' || typeof authUrls === 'string') {
      dispatch({
        type: 'UPDATE_STATE',
        payload: { fetching: false } as SessionContext,
      });
      return false;
    }

    dispatch({
      type: 'UPDATE_STATE',
      payload: {
        ...session,
        ...authUrls,
        fetching: false
      } as SessionContext,
    });

    return true;
  };

  const login = (username: string, password: string) => {
    dispatch({
      type: 'UPDATE_STATE',
      payload: { fetching: true } as SessionContext,
    });
    return loginApiCall(username, password)
      .then((success) => {
        if (typeof success === 'string') {
          toast({
            title: 'Login Error',
            description: success,
            variant: 'destructive'
          });
          dispatch({
            type: 'UPDATE_STATE',
            payload: { fetching: false } as SessionContext,
          });
          return false;
        }
        
        return fetchSession();
    });
  };

  // Fetch 고객 데이터.
  const fetchSession = () => {
    dispatch({
      type: 'UPDATE_STATE',
      payload: { fetching: true } as SessionContext,
    });

    return getSessionApiCall()
      .then(async (sessionPayload) => {
        const authUrlPayload = await fetchAuthURLsApiCall();
        return setSession(sessionPayload, authUrlPayload);
      });
  };

  useEffect(() => {
    if (state.fetching) {
      return;
    }

    if (!state.customer || !state.cart) {
      fetchSession();
    }
  }, []);
      

  const store: SessionContext = {
    ...
    refetch: fetchSession,
    login,
  };
  return (
    <Provider value={store}>{children}</Provider>
  );
}

export const useSession = () => useContext(sessionContext);

SessionProvider를 업데이트 한 후, 로그인 및 기타 사용자 Nav 옵션은 예상대로 작동해야합니다. WP 설치를 스타일링 할 필요가 있지만 다음 응용 프로그램을 완벽하게 경험할 수 있습니다.

관련 기사

다음 튜토리얼에서 우리는 단일 제품 페이지 및 카트 옵션으로 우리의 응용 프로그램을 완료 할 것입니다. 희망,이 튜토리얼은 당신이 즐겁게 유지하고 나는 다음 하나에서 당신을 볼 수 있습니다.

마지막 장으로 계속



Leave a Reply

Your email address will not be published. Required fields are marked *