抬头: 此教程涵盖较早的 WooGraphQL 发布, 并且不再更新 。 重写版本正在展开,
目录
欢迎来到我们教程系列的下一个分期, 在那里,我们将丰富我们的eCommerce商店 通过包含用户导航和登录页面。 本章的内容是增强用户界面和互动能力,从而创造出更具接触性和无缝用户体验.
创建用户登录和导航
eCommerce网站的核心功能之一是用户能够浏览网站的不同部分,并有能力登录和管理其账户。 顶端通航栏作为通航的中心枢纽,使用户能够方便而高效地访问网站的各个部分. 在顶端导航栏上添加用户导航不仅会提高可用性,而且会帮助用户无心地访问其账户和推车细节.
此外,我们将带领您通过在您的eCommerce商店中添加登录页面的过程. 一个设计良好的登录页面对于任何在线商店都至关重要,因为它是用户体验个性化的网关,允许用户跟踪订单,保存自己最喜爱的产品,并加快取出过程.
为了让这笔交易更贴切,我们将探索一个能节省你时间和精力的巧妙伎俩. 我们会告诉你如何利用会话的下放链接 通过终端用户的会话 从我们的Next.js应用程序到WordPress的安装 托管我们的WooCommerce商店。 这种方法的好处是,它使我们能够绕过在我们的应用程序中创建某些网页的需要,特别是那些可能花费时间来构建的网页,或者那些依赖于我们前端应用程序中不易提供的功能的网页.
先决条件
在开始之前,我们应该更新我们 .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,
},
}
...
到本教程结束后,您将获得改善用户导航的知识,加入一个方便用户的登录页面,并实现会话下放链接,将您的eCommerce商店带入下一层用户交互和功能. 让我们潜入并开始!
第一部分 创建登录页面和用户Nav
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数字 :
// 客户端/ Login/ 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 是钩子 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>
)
}第二部分:创建会话
1. 联合国 创建 /client/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数字 :
// 应用程序/layout.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>
)
}有了这个应用程序,将成功构建和运行,但是我们引入的组件都没有实际工作. 那些有敏锐眼光的人会注意到,我们还没有执行任何逻辑来与我们的终点进行交流.

这是因为,在处理最终用户的安全问题时,需要对会议全权证书进行精密管理并限制GraphQL终点的能见度。 需要注意的是,尽管至今已经创建了各种功能,但是在显示GraphQL端点的活应用程序中还是有动作发生. 别相信 打开您的浏览器开发工具并检查 Network 选项卡。 刷新页面并单击周围, 做任何事情, 但是您不会看到对您的 GraphQL 端点的可见请求 。
直到现在为止,所有的GraphQL请求都是在服务器级别上提出的,只在页面构建时间的每次运行中,但我们必须提出一些客户端请求,以应对终端用户的会话,如果我们直接运行GraphQL查询到客户端,就会暴露端点.
所以我们不会在客户端上运行它们 而是利用下13路的网页 我们将制作几个路由页面, 以运行客户端的 GraphQL 请求 。 在此之前,我们将需要创建一些效用文件,以定义与这些路由页面沟通的逻辑,并管理时间敏感的会话权限. 我们开始吧
第3部分:创建工具文件.
1. 联合国 安装 crypto-js 和 jwt-decode 软件包 :
npm install crypto-js jwt-decode这些库可以重新创建WordPress的散列功能.
2. 联合国 创建 /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);
// 创建非ce
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;
}我不打算在文件上谈很多细节 但主要是JS的克隆人 几个PHP/WP的功能/constants 至于我们 session 我们关心的公用事业 time 函数和 MINUTE_IN_SECONDS 常数。 前进
3个 创建 /utils/client.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)) {
// 与用户代理创建证书对象 。
credentials = {
userAgent: window?.navigator?.userAgent || '',
ip: '',
issued: 0,
};
// 获取IP。
const response = await fetch('https://api.ipify.org/?format=json');
const { data } = await response.json();
credentials.ip = data?.ip || '';
}
// 更新时间戳,以确保每次生成新的非切
// 终端用户启动该应用程序。
credentials.issued = time();
localStorage.setItem(process.env.CLIENT_CREDENTIALS_LS_KEY as string, JSON.stringify(credentials));
// 生成客户端会话编号 。
const clientSessionId = wpNonceHash(JSON.stringify(credentials));
const timeout = `${credentials.issued + HOUR_IN_SECONDS}`;
// 保存客户端会话编号 。
sessionStorage.setItem(process.env.CLIENT_SESSION_SS_KEY as string, clientSessionId);
sessionStorage.setItem(process.env.CLIENT_SESSION_EXP_SS_KEY as string, timeout);
// 返回客户端会话编号 。
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;
}
// 认证管理.
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 无控制台
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:首先从用户获取最终用户的会话全权证书。localStorage然后发送 POST 请求/api/session与最终用户的authToken和sessionToken用来确认身份。- 如果最终用户有
refreshToken无效authToken新建authToken通过将 POST 请求发送给/api/auth内fetchAuthToken, (中文(简体) ). - 如果最终用户没有新的身份证明
sessionToken通过发送 GET 请求到/api/auth内fetchSessionToken
- 如果最终用户有
fetchAuthURLs: 工作完全一样getSession除非它发出clientSessionId和clientSessionIdTimeout以及会话下放 URL 生成。- 如果最终用户的无效
clientSessionId,生成了一个新的。
- 如果最终用户的无效
创建了所有的工具文件 。 让我们转到路面页面。
第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假设用户是新人且没有证书,并返回新的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 在最终用户的WooCommerce会话中,服务器上的对象。 随后这些值被用来生成WP后端上的一系列非切,用于会话下放的URL.
现在我们可以在GraphQL端点上查询这些非Ces甚至整个会话下放的URL,然而,这些非Ces/URLs在中转中泄露的风险。 因此,我们利用了其中定义的其他功能。 utils/nonce.ts 在我们的路由页面中重新创建完全相同的非Ces。 generateUrl 生成一个为终端用户量身定制的会话投放 URL, 并绑定在该机上。 最后的路由页面已经创建,剩下的都是更新 SessionProvider 以使用工具文件和扩展路由页面。
第5编:完成 SessionProvider
1. 联合国 更新 /client/SessionProvider/SessionProvider.tsx数字 :
// 客户端/会议提供/会议提供.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();
};
// 进程会话获取请求响应 。
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();
});
};
// 获取客户数据 。
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);在更新会话提供程序后,登录和其它用户导航选项应该如预期的那样工作,尽管您可能需要样式化您的WP安装,以看起来像您 下一个无缝体验的应用程序 。

结论
接下来的辅导中,我们将用单一的产品页和推车选项完成我们的应用. 希望这间教程能让你玩得开心 下次见

Leave a Reply