맨 위: 이 튜토리얼은 이전 WooGraphQL 릴리스를 다루고 더 이상 날짜가 없습니다. rewritten version은 진행 중입니다. 이 버전은 여기에서 공개됩니다.
본문 바로가기
- Headless eCommerce 응용 프로그램을위한 WordPress 설정
- eCommerce 앱 만들기 및 설정
- 빌딩 숍 목록 페이지 WooGraphQL
- 사용자 로그인 및 탐색
- 제품 페이지 및 카트 옵션 만들기
우리의 튜토리얼 시리즈의 다음 설치에 오신 것을 환영합니다, 우리는 사용자 탐색 및 로그인 페이지를 통합하여 전자 상거래 매장을 풍부하게합니다. 이 장은 사용자 인터페이스와 상호 작용 능력을 강화하는 데 관한 모든 것입니다. 더 많은 참여와 원활한 사용자 경험을 만들 수 있습니다.
사용자 로그인 및 탐색
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·sessionTokenID를 위해.- 최종 사용자의 경우
refreshToken그리고 유효하지 않음authToken그리고 새로운authTokenPOST 요청을 전송하여 검색/api/auth내 계정fetchAuthToken· - 최종 사용자의 자격은 새로운 것
sessionTokenGET 요청을 전송하여 검색/api/auth내 계정fetchSessionToken
- 최종 사용자의 경우
fetchAuthURLs: 정확히 같은 작품getSession그것을 제외하고clientSessionId·clientSessionIdTimeout또한 세션 드롭오프 URL 생성을 위해.- end-user가 유효하지 않은 경우
clientSessionId, 새로운 것은 생성됩니다.
- end-user가 유효하지 않은 경우
만든 모든 유틸리티 파일로. 노선 페이지로 이동합시다.
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