mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 23:46:58 +00:00
auth proccess
This commit is contained in:
parent
27facd7520
commit
77408259d2
@ -1,21 +0,0 @@
|
||||
'use client';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export default function Error({ reset }: { reset: () => void }) {
|
||||
return (
|
||||
<div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 dark:border-neutral-800 dark:bg-black md:p-12">
|
||||
<h2 className="text-xl font-bold">Oh no!</h2>
|
||||
<p className="my-2">
|
||||
There was an issue with our storefront. This could be a temporary issue, please try your
|
||||
action again.
|
||||
</p>
|
||||
<button
|
||||
className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
50
app/account/component/AccountBook.tsx
Normal file
50
app/account/component/AccountBook.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import AddressCard from '@/components/AddressCard';
|
||||
import { Button } from '@/components/Button';
|
||||
import { Text } from '@/components/Text';
|
||||
import { Customer, MailingAddress } from '@/lib/shopify/types';
|
||||
import { convertObjectToQueryString } from '@/lib/utils';
|
||||
|
||||
export default function AccountBook({
|
||||
customer,
|
||||
addresses,
|
||||
}: {
|
||||
customer: Customer;
|
||||
addresses: MailingAddress[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h3 className="font-bold text-lead">Address Book</h3>
|
||||
<div>
|
||||
{!addresses?.length && (
|
||||
<Text className="mb-1" width="narrow" as="p" size="copy">
|
||||
You haven't saved any addresses yet.
|
||||
</Text>
|
||||
)}
|
||||
<div className="w-48">
|
||||
<a
|
||||
href={`account?${convertObjectToQueryString({
|
||||
modal: 'address-add',
|
||||
})}`}
|
||||
className="inline-block rounded font-medium text-center py-3 px-6 border border-primary/10 bg-contrast text-primary mt-2 text-sm w-full mb-6"
|
||||
>
|
||||
Add an Address
|
||||
</a>
|
||||
</div>
|
||||
{Boolean(addresses?.length) && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{customer.defaultAddress && (
|
||||
<AddressCard address={customer.defaultAddress} defaultAddress />
|
||||
)}
|
||||
{addresses
|
||||
.filter(address => address.id !== customer.defaultAddress?.id)
|
||||
.map(address => (
|
||||
<AddressCard key={address.id} address={address} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
11
app/account/component/AuthLayout.tsx
Normal file
11
app/account/component/AuthLayout.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-center my-24 px-4">
|
||||
<div className="max-w-md w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
22
app/account/component/FormButton.tsx
Normal file
22
app/account/component/FormButton.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
import cn from 'clsx';
|
||||
export default function FormButton({
|
||||
variant = 'primary'
|
||||
}: {
|
||||
btnText: string;
|
||||
state?: string;
|
||||
variant?: 'primary' | 'outline';
|
||||
}) {
|
||||
const buttonClasses = cn({
|
||||
'bg-primary text-contrast rounded py-2 px-4 focus:shadow-outline block w-full':
|
||||
variant === 'primary',
|
||||
'text-left text-primary/50 ml-6 text-sm': variant === 'outline'
|
||||
});
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<button className={buttonClasses} type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
45
app/account/component/FormFooter.tsx
Normal file
45
app/account/component/FormFooter.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
interface IFormFooter {
|
||||
page: 'login' | 'register' | 'recover';
|
||||
}
|
||||
|
||||
export default function FormFooter({ page }: IFormFooter) {
|
||||
const data = {
|
||||
login: {
|
||||
linkText: 'Create an account',
|
||||
phrase: 'New to Hydrogen?',
|
||||
href: '/account/register',
|
||||
},
|
||||
register: {
|
||||
linkText: 'Sign In',
|
||||
phrase: 'Already have an account?',
|
||||
href: '/account/login',
|
||||
},
|
||||
recover: {
|
||||
linkText: 'Login',
|
||||
phrase: 'Return to',
|
||||
href: '/account/login',
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-5 mt-8 border-t border-gray-300">
|
||||
<p className="align-baseline text-sm mt-6">
|
||||
{data[page].phrase}
|
||||
|
||||
<Link className="inline underline" href={data[page].href}>
|
||||
{data[page].linkText}
|
||||
</Link>
|
||||
</p>
|
||||
{page === 'login' && (
|
||||
<Link
|
||||
className="mt-6 inline-block align-baseline text-sm text-primary/50x"
|
||||
href="/account/recover"
|
||||
>
|
||||
Forgot Password
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
3
app/account/component/FormHeader.tsx
Normal file
3
app/account/component/FormHeader.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function FormHeader({ title }: { title: string }) {
|
||||
return <h1 className="text-4xl">{title}</h1>;
|
||||
}
|
14
app/account/component/OrderHistory.tsx
Normal file
14
app/account/component/OrderHistory.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import EmptyOrders from '@/components/EmptyOrder';
|
||||
import Orders from '@/components/Orders';
|
||||
import { Order } from '@/lib/shopify/types';
|
||||
|
||||
export default function OrderHistory({ orders }: { orders: Order[] }) {
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div className="grid w-full gap-4 p-4 py-6 md:gap-8 md:p-8 lg:p-12">
|
||||
<h2 className="font-bold text-lead">Order History</h2>
|
||||
{orders?.length ? <Orders orders={orders} /> : <EmptyOrders />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
app/account/component/SignOutSection.tsx
Normal file
23
app/account/component/SignOutSection.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export default function SignOutSection() {
|
||||
const signOut = async () => {
|
||||
'use server';
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value: '',
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now()),
|
||||
});
|
||||
redirect('/account/login');
|
||||
};
|
||||
return (
|
||||
<form action={signOut} noValidate>
|
||||
<button type="submit" className="text-primary/50">
|
||||
Sign out
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
114
app/account/login/page.tsx
Normal file
114
app/account/login/page.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { loginCustomer } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import AuthLayout from '../component/AuthLayout';
|
||||
import FormButton from '../component/FormButton';
|
||||
import FormFooter from '../component/FormFooter';
|
||||
import FormHeader from '../component/FormHeader';
|
||||
|
||||
let emailError: string | null = null;
|
||||
let passwordError: string | null = null;
|
||||
let unidentifiedUserError: string | null = null;
|
||||
export default function LoginPage() {
|
||||
async function handleSubmit(data: FormData) {
|
||||
'use server';
|
||||
const loginRes = await loginCustomer({
|
||||
variables: {
|
||||
input: {
|
||||
email: data.get('email') as string,
|
||||
password: data.get('password') as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
?.accessToken
|
||||
) {
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value:
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
.accessToken,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + 20 * 60 * 1000 + 5 * 1000),
|
||||
});
|
||||
redirect('/account');
|
||||
}
|
||||
|
||||
if (
|
||||
loginRes.body.data.customerAccessTokenCreate.customerUserErrors.length > 0
|
||||
) {
|
||||
loginRes.body.data.customerAccessTokenCreate.customerUserErrors.filter(
|
||||
(error: any) => {
|
||||
if (error.field) {
|
||||
if (error.field.includes('email')) {
|
||||
emailError = error.message;
|
||||
}
|
||||
if (error.field.includes('password')) {
|
||||
passwordError = error.message;
|
||||
}
|
||||
} else {
|
||||
if (error.code === 'UNIDENTIFIED_CUSTOMER') {
|
||||
unidentifiedUserError = error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
revalidatePath('/account/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title="Sign in." />
|
||||
{unidentifiedUserError && (
|
||||
<p className="text-red-500 mt-4">{unidentifiedUserError}</p>
|
||||
)}
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1}`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autoFocus
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-xs">{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-red-500 text-xs"> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText="Sign in" />
|
||||
<FormFooter page="login" />
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
19
app/account/page.tsx
Normal file
19
app/account/page.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { getCustomer } from 'lib/shopify';
|
||||
import { cookies } from 'next/headers';
|
||||
import SignOutSection from './component/SignOutSection';
|
||||
|
||||
async function AccountPage({ searchParams }: { searchParams: { [key: string]: string } }) {
|
||||
const token = cookies().get('customerAccessToken')?.value as string;
|
||||
const customer = await getCustomer(token);
|
||||
console.log('customer', customer);
|
||||
console.log('token', token);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SignOutSection />
|
||||
Account Detail
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountPage;
|
89
app/account/recover/page.tsx
Normal file
89
app/account/recover/page.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import { recoverCustomersPassword } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import AuthLayout from '../component/AuthLayout';
|
||||
import FormButton from '../component/FormButton';
|
||||
import FormFooter from '../component/FormFooter';
|
||||
import FormHeader from '../component/FormHeader';
|
||||
|
||||
let emailError: string | null = null;
|
||||
let isSubmited: boolean = false;
|
||||
const headings = {
|
||||
submited: {
|
||||
title: 'Request Sent.',
|
||||
description:
|
||||
'If that email address is in our system, you will receive an email with instructions about how to reset your password in a few minutes.',
|
||||
},
|
||||
default: {
|
||||
title: 'Forgot Password.',
|
||||
description:
|
||||
'Enter the email address associated with your account to receive a link to reset your password.',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RecoverPassword() {
|
||||
async function handleSubmit(data: FormData) {
|
||||
'use server';
|
||||
try {
|
||||
const response = await recoverCustomersPassword({
|
||||
variables: {
|
||||
email: data.get('email') as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body.data.customerRecover.customerUserErrors.length > 0) {
|
||||
response.body.data.customerRecover.customerUserErrors.filter(
|
||||
(error: any) => {
|
||||
if (error.field && error.field.includes('email')) {
|
||||
emailError = error.message;
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
isSubmited = true;
|
||||
}
|
||||
} catch (error) {
|
||||
interface ERROR {
|
||||
message: string;
|
||||
}
|
||||
const err = error as { error: ERROR };
|
||||
emailError = err.error.message;
|
||||
}
|
||||
|
||||
revalidatePath('/account/recover');
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title={headings[isSubmited ? 'submited' : 'default'].title} />
|
||||
<p className="mt-4">
|
||||
{headings[isSubmited ? 'submited' : 'default'].description}
|
||||
</p>
|
||||
{!isSubmited && (
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autoFocus
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-xs">{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText={'Request Reset Link'} />
|
||||
<FormFooter page="recover" />
|
||||
</form>
|
||||
)}
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
114
app/account/register/page.tsx
Normal file
114
app/account/register/page.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { createCustomer, loginCustomer } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import AuthLayout from '../component/AuthLayout';
|
||||
import FormButton from '../component/FormButton';
|
||||
import FormFooter from '../component/FormFooter';
|
||||
import FormHeader from '../component/FormHeader';
|
||||
|
||||
let emailError: string | null = null;
|
||||
let passwordError: string | null = null;
|
||||
|
||||
export default function RegisterPage() {
|
||||
async function handleSubmit(data: FormData) {
|
||||
'use server';
|
||||
const res = await createCustomer({
|
||||
variables: {
|
||||
input: {
|
||||
email: data.get('email') as string,
|
||||
password: data.get('password') as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (res.body.data.customerCreate.customer) {
|
||||
const loginRes = await loginCustomer({
|
||||
variables: {
|
||||
input: {
|
||||
email: data.get('email') as string,
|
||||
password: data.get('password') as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
?.accessToken
|
||||
) {
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value:
|
||||
loginRes.body.data.customerAccessTokenCreate.customerAccessToken
|
||||
.accessToken,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + 20 * 60 * 1000 + 5 * 1000),
|
||||
});
|
||||
redirect('/account');
|
||||
}
|
||||
|
||||
redirect('/account/login');
|
||||
}
|
||||
|
||||
if (res.body.data.customerCreate.customerUserErrors.length > 0) {
|
||||
res.body.data.customerCreate.customerUserErrors.filter((error: any) => {
|
||||
if (error.field.includes('email')) {
|
||||
emailError = error.message;
|
||||
}
|
||||
if (error.field.includes('password')) {
|
||||
passwordError = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
revalidatePath('/account/register');
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title="Create an Account" />
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
aria-label="Email address"
|
||||
autoFocus
|
||||
/>
|
||||
{emailError && (
|
||||
<p className="text-red-500 text-xs">{emailError} </p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-red-500 text-xs"> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText="Create Account" />
|
||||
<FormFooter page="register" />
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
131
app/account/reset/[id]/[resetToken]/page.tsx
Normal file
131
app/account/reset/[id]/[resetToken]/page.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { resetCustomersPassword } from 'lib/shopify';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { cookies } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import AuthLayout from '../../../component/AuthLayout';
|
||||
import FormButton from '../../../component/FormButton';
|
||||
import FormHeader from '../../../component/FormHeader';
|
||||
|
||||
let errorMessage: string | null = null;
|
||||
let passwordError: string | null = null;
|
||||
let passwordConfirmError: string | null = null;
|
||||
|
||||
export default function ResetPassword({
|
||||
params,
|
||||
}: {
|
||||
params: { id: string; resetToken: string };
|
||||
}) {
|
||||
const handleSubmit = async (data: FormData) => {
|
||||
'use server';
|
||||
const id = params.id;
|
||||
const resetToken = params.resetToken;
|
||||
|
||||
const password = data.get('password') as string;
|
||||
const passwordConfirm = data.get('passwordConfirm') as string;
|
||||
|
||||
if (
|
||||
!password ||
|
||||
!passwordConfirm ||
|
||||
typeof password !== 'string' ||
|
||||
typeof passwordConfirm !== 'string' ||
|
||||
password !== passwordConfirm
|
||||
) {
|
||||
passwordConfirmError = 'The two passwords entered did not match.';
|
||||
} else {
|
||||
const res = await resetCustomersPassword({
|
||||
variables: {
|
||||
id: `gid://shopify/Customer/${id}`,
|
||||
input: {
|
||||
password,
|
||||
resetToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const customerAccessToken =
|
||||
res.body.data.customerReset.customerAccessToken;
|
||||
|
||||
if (customerAccessToken) {
|
||||
const accessToken = customerAccessToken?.accessToken;
|
||||
cookies().set({
|
||||
name: 'customerAccessToken',
|
||||
value: accessToken,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
expires: new Date(Date.now() + 20 * 60 * 1000 + 5 * 1000),
|
||||
});
|
||||
redirect('/account');
|
||||
}
|
||||
|
||||
if (res.body.data.customerReset.customerUserErrors.length > 0) {
|
||||
res.body.data.customerReset.customerUserErrors.filter((error: any) => {
|
||||
if (error.field) {
|
||||
if (error.field.includes('password')) {
|
||||
passwordError = error.message;
|
||||
} else if (error.field.includes('passwordConfirm')) {
|
||||
passwordConfirmError = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (error.code === 'TOKEN_INVALID') {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
revalidatePath('/account/reset');
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout>
|
||||
<FormHeader title="Reset Password." />
|
||||
<p className="mt-4">Enter a new password for your account.</p>
|
||||
{errorMessage && <p className="text-red-500 mt-4">{errorMessage}</p>}
|
||||
<form
|
||||
action={handleSubmit}
|
||||
noValidate
|
||||
className="pt-6 pb-8 mt-4 mb-4 space-y-3"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Password"
|
||||
aria-label="Password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordError && (
|
||||
<p className="text-red-500 text-xs"> {passwordError} </p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
className={`mb-1`}
|
||||
id="passwordConfirm"
|
||||
name="passwordConfirm"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="Re-enter password"
|
||||
aria-label="Re-enter password"
|
||||
minLength={8}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
{passwordConfirmError && (
|
||||
<p className="text-red-500 text-xs">
|
||||
{' '}
|
||||
{passwordConfirmError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<FormButton btnText={'Save'} />
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
8
lib/isAuthenticated.ts
Normal file
8
lib/isAuthenticated.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
const isAuthenticated = (request: NextRequest) => {
|
||||
const customerAccessToken = request.cookies.get('customerAccessToken')?.value;
|
||||
return customerAccessToken;
|
||||
};
|
||||
|
||||
export default isAuthenticated;
|
@ -4,6 +4,12 @@ import { ensureStartsWith } from 'lib/utils';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
CUSTOMER_CREATE_MUTATION,
|
||||
CUSTOMER_RECOVER_MUTATION,
|
||||
CUSTOMER_RESET_MUTATION,
|
||||
LOGIN_MUTATION,
|
||||
} from './mutations/auth';
|
||||
import {
|
||||
addToCartMutation,
|
||||
createCartMutation,
|
||||
@ -23,10 +29,16 @@ import {
|
||||
getProductRecommendationsQuery,
|
||||
getProductsQuery
|
||||
} from './queries/product';
|
||||
import { CUSTOMER_QUERY } from './queries/user';
|
||||
import {
|
||||
Cart,
|
||||
Collection,
|
||||
Connection,
|
||||
Customer,
|
||||
CustomerAccessTokenCreatePayload,
|
||||
CustomerCreatePayload,
|
||||
CustomerRecoverPayload,
|
||||
CustomerResetPayload,
|
||||
Image,
|
||||
Menu,
|
||||
Page,
|
||||
@ -47,7 +59,7 @@ import {
|
||||
ShopifyProductRecommendationsOperation,
|
||||
ShopifyProductsOperation,
|
||||
ShopifyRemoveFromCartOperation,
|
||||
ShopifyUpdateCartOperation
|
||||
ShopifyUpdateCartOperation,
|
||||
} from './types';
|
||||
|
||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||
@ -282,6 +294,135 @@ export async function getCollection(handle: string): Promise<Collection | undefi
|
||||
return reshapeCollection(res.body.data.collection);
|
||||
}
|
||||
|
||||
export async function createCustomer({
|
||||
variables,
|
||||
}: {
|
||||
variables: {
|
||||
input: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const data = await shopifyFetch<{
|
||||
data: {
|
||||
customerCreate: CustomerCreatePayload;
|
||||
};
|
||||
variables: {
|
||||
input: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}>({
|
||||
query: CUSTOMER_CREATE_MUTATION,
|
||||
variables,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function loginCustomer({
|
||||
variables,
|
||||
}: {
|
||||
variables: {
|
||||
input: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const data = await shopifyFetch<{
|
||||
data: {
|
||||
customerAccessTokenCreate: CustomerAccessTokenCreatePayload;
|
||||
};
|
||||
variables: {
|
||||
input: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}>({
|
||||
query: LOGIN_MUTATION,
|
||||
variables,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function recoverCustomersPassword({
|
||||
variables,
|
||||
}: {
|
||||
variables: {
|
||||
email: string;
|
||||
};
|
||||
}) {
|
||||
const data = await shopifyFetch<{
|
||||
data: {
|
||||
customerRecover: CustomerRecoverPayload;
|
||||
};
|
||||
variables: {
|
||||
email: string;
|
||||
};
|
||||
}>({
|
||||
query: CUSTOMER_RECOVER_MUTATION,
|
||||
variables,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function resetCustomersPassword({
|
||||
variables,
|
||||
}: {
|
||||
variables: {
|
||||
id: string;
|
||||
input: {
|
||||
password: string;
|
||||
resetToken: string;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const data = await shopifyFetch<{
|
||||
data: {
|
||||
customerReset: CustomerResetPayload;
|
||||
};
|
||||
variables: {
|
||||
id: string;
|
||||
input: {
|
||||
password: string;
|
||||
resetToken: string;
|
||||
};
|
||||
};
|
||||
}>({
|
||||
query: CUSTOMER_RESET_MUTATION,
|
||||
variables,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCustomer(
|
||||
customerAccessToken: string
|
||||
): Promise<Customer> {
|
||||
const res = await shopifyFetch<{
|
||||
data: { customer: Customer };
|
||||
variables: {
|
||||
customerAccessToken: string;
|
||||
};
|
||||
}>({
|
||||
query: CUSTOMER_QUERY,
|
||||
variables: {
|
||||
customerAccessToken,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* If the customer failed to load, we assume their access token is invalid.
|
||||
*/
|
||||
if (!res || !res.body.data.customer) {
|
||||
// log out customer
|
||||
}
|
||||
|
||||
return res.body.data.customer;
|
||||
}
|
||||
|
||||
export async function getCollectionProducts({
|
||||
collection,
|
||||
reverse,
|
||||
|
58
lib/shopify/mutations/auth.ts
Normal file
58
lib/shopify/mutations/auth.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export const CUSTOMER_CREATE_MUTATION = `#graphql
|
||||
mutation customerCreate($input: CustomerCreateInput!) {
|
||||
customerCreate(input: $input) {
|
||||
customer {
|
||||
id
|
||||
}
|
||||
customerUserErrors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LOGIN_MUTATION = `#graphql
|
||||
mutation customerAccessTokenCreate($input: CustomerAccessTokenCreateInput!) {
|
||||
customerAccessTokenCreate(input: $input) {
|
||||
customerUserErrors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
customerAccessToken {
|
||||
accessToken
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CUSTOMER_RECOVER_MUTATION = `#graphql
|
||||
mutation customerRecover($email: String!) {
|
||||
customerRecover(email: $email) {
|
||||
customerUserErrors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CUSTOMER_RESET_MUTATION = `#graphql
|
||||
mutation customerReset($id: ID!, $input: CustomerResetInput!) {
|
||||
customerReset(id: $id, input: $input) {
|
||||
customerAccessToken {
|
||||
accessToken
|
||||
expiresAt
|
||||
}
|
||||
customerUserErrors {
|
||||
code
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
76
lib/shopify/queries/user.ts
Normal file
76
lib/shopify/queries/user.ts
Normal file
@ -0,0 +1,76 @@
|
||||
export const CUSTOMER_QUERY = `#graphql
|
||||
query CustomerDetails(
|
||||
$customerAccessToken: String!
|
||||
$country: CountryCode
|
||||
$language: LanguageCode
|
||||
) @inContext(country: $country, language: $language) {
|
||||
customer(customerAccessToken: $customerAccessToken) {
|
||||
firstName
|
||||
lastName
|
||||
phone
|
||||
email
|
||||
defaultAddress {
|
||||
id
|
||||
formatted
|
||||
firstName
|
||||
lastName
|
||||
company
|
||||
address1
|
||||
address2
|
||||
country
|
||||
province
|
||||
city
|
||||
zip
|
||||
phone
|
||||
}
|
||||
addresses(first: 6) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
formatted
|
||||
firstName
|
||||
lastName
|
||||
company
|
||||
address1
|
||||
address2
|
||||
country
|
||||
province
|
||||
city
|
||||
zip
|
||||
phone
|
||||
}
|
||||
}
|
||||
}
|
||||
orders(first: 250, sortKey: PROCESSED_AT, reverse: true) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
orderNumber
|
||||
processedAt
|
||||
financialStatus
|
||||
fulfillmentStatus
|
||||
currentTotalPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
lineItems(first: 2) {
|
||||
edges {
|
||||
node {
|
||||
variant {
|
||||
image {
|
||||
url
|
||||
altText
|
||||
height
|
||||
width
|
||||
}
|
||||
}
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -263,3 +263,143 @@ export type ShopifyProductsOperation = {
|
||||
sortKey?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Customer = {
|
||||
__typename?: 'Customer';
|
||||
/** Indicates whether the customer has consented to be sent marketing material via email. */
|
||||
acceptsMarketing: boolean;
|
||||
/** A list of addresses for the customer. */
|
||||
addresses: Maybe<string>;
|
||||
/** The date and time when the customer was created. */
|
||||
createdAt: string;
|
||||
/** The customer’s default address. */
|
||||
defaultAddress?: Maybe<string>;
|
||||
/** The customer’s name, email or phone number. */
|
||||
displayName: string;
|
||||
/** The customer’s email address. */
|
||||
email?: Maybe<string>;
|
||||
/** The customer’s first name. */
|
||||
firstName?: Maybe<string>;
|
||||
/** A unique identifier for the customer. */
|
||||
id: string;
|
||||
/** The customer's most recently updated, incomplete checkout. */
|
||||
/** The customer’s last name. */
|
||||
lastName?: Maybe<string>;
|
||||
/** Returns a metafield found by namespace and key. */
|
||||
/**
|
||||
* The metafields associated with the resource matching the supplied list of namespaces and keys.
|
||||
*
|
||||
*/
|
||||
/** The number of orders that the customer has made at the store in their lifetime. */
|
||||
numberOfOrders: string;
|
||||
/** The orders associated with the customer. */
|
||||
/** The customer’s phone number. */
|
||||
phone?: Maybe<string>;
|
||||
/**
|
||||
* A comma separated list of tags that have been added to the customer.
|
||||
* Additional access scope required: unauthenticated_read_customer_tags.
|
||||
*
|
||||
*/
|
||||
tags: Array<string>;
|
||||
/** The date and time when the customer information was updated. */
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Scalars = {
|
||||
ID: string;
|
||||
String: string;
|
||||
Boolean: boolean;
|
||||
Int: number;
|
||||
Float: number;
|
||||
Color: string;
|
||||
DateTime: string;
|
||||
Decimal: string;
|
||||
HTML: string;
|
||||
JSON: unknown;
|
||||
URL: string;
|
||||
UnsignedInt64: string;
|
||||
};
|
||||
|
||||
export type DisplayableError = {
|
||||
/** The path to the input field that caused the error. */
|
||||
field?: Maybe<Array<Scalars['String']>>;
|
||||
/** The error message. */
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CustomerAccessToken = {
|
||||
__typename?: 'CustomerAccessToken';
|
||||
/** The customer’s access token. */
|
||||
accessToken: Scalars['String'];
|
||||
/** The date and time when the customer access token expires. */
|
||||
expiresAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
export type CustomerUserError = DisplayableError & {
|
||||
__typename?: 'CustomerUserError';
|
||||
/** The error code. */
|
||||
code?: Maybe<any>;
|
||||
/** The path to the input field that caused the error. */
|
||||
field?: Maybe<Array<Scalars['String']>>;
|
||||
/** The error message. */
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UserError = DisplayableError & {
|
||||
__typename?: 'UserError';
|
||||
/** The path to the input field that caused the error. */
|
||||
field?: Maybe<Array<Scalars['String']>>;
|
||||
/** The error message. */
|
||||
message: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CustomerAccessTokenCreatePayload = {
|
||||
__typename?: 'CustomerAccessTokenCreatePayload';
|
||||
/** The newly created customer access token object. */
|
||||
customerAccessToken?: Maybe<CustomerAccessToken>;
|
||||
/** The list of errors that occurred from executing the mutation. */
|
||||
customerUserErrors: Array<CustomerUserError>;
|
||||
/**
|
||||
* The list of errors that occurred from executing the mutation.
|
||||
* @deprecated Use `customerUserErrors` instead.
|
||||
*/
|
||||
userErrors: Array<UserError>;
|
||||
};
|
||||
|
||||
/** Return type for `customerCreate` mutation. */
|
||||
export type CustomerCreatePayload = {
|
||||
__typename?: 'CustomerCreatePayload';
|
||||
/** The created customer object. */
|
||||
customer?: Maybe<Customer>;
|
||||
/** The list of errors that occurred from executing the mutation. */
|
||||
/**
|
||||
* The list of errors that occurred from executing the mutation.
|
||||
* @deprecated Use `customerUserErrors` instead.
|
||||
*/
|
||||
};
|
||||
|
||||
export type CustomerRecoverPayload = {
|
||||
__typename?: 'CustomerRecoverPayload';
|
||||
/** The list of errors that occurred from executing the mutation. */
|
||||
customerUserErrors: Array<CustomerUserError>;
|
||||
/**
|
||||
* The list of errors that occurred from executing the mutation.
|
||||
* @deprecated Use `customerUserErrors` instead.
|
||||
*/
|
||||
userErrors: Array<UserError>;
|
||||
};
|
||||
|
||||
export type CustomerResetPayload = {
|
||||
__typename?: 'CustomerResetPayload';
|
||||
/** The customer object which was reset. */
|
||||
customer?: Maybe<Customer>;
|
||||
/** A newly created customer access token object for the customer. */
|
||||
customerAccessToken?: Maybe<CustomerAccessToken>;
|
||||
/** The list of errors that occurred from executing the mutation. */
|
||||
customerUserErrors: Array<CustomerUserError>;
|
||||
/**
|
||||
* The list of errors that occurred from executing the mutation.
|
||||
* @deprecated Use `customerUserErrors` instead.
|
||||
*/
|
||||
userErrors: Array<UserError>;
|
||||
};
|
||||
|
29
middleware.ts
Normal file
29
middleware.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import isAuthenticated from './lib/isAuthenticated';
|
||||
|
||||
export const config = {
|
||||
matcher: ['/checkout', '/account', '/account/:path*'],
|
||||
};
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const isLoginPage = request.nextUrl.pathname === '/account/login';
|
||||
const isRecoverPasswordPage =
|
||||
request.nextUrl.pathname.startsWith('/account/recover');
|
||||
const isResetPasswordPage =
|
||||
request.nextUrl.pathname.startsWith('/account/reset');
|
||||
const isRegisterPage = request.nextUrl.pathname === '/account/register';
|
||||
|
||||
const authPages =
|
||||
isLoginPage ||
|
||||
isRecoverPasswordPage ||
|
||||
isRegisterPage ||
|
||||
isResetPasswordPage;
|
||||
|
||||
if (authPages && isAuthenticated(request)) {
|
||||
return NextResponse.redirect(new URL('/account', request.url));
|
||||
}
|
||||
|
||||
if (!authPages && !isAuthenticated(request)) {
|
||||
return NextResponse.redirect(new URL('/account/login', request.url));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user