feat: adds gql auth example (#2115)
This commit is contained in:
34
examples/auth/nextjs/components/Auth/gql.ts
Normal file
34
examples/auth/nextjs/components/Auth/gql.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const USER = `
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
`
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const gql = async (query): Promise<any> => {
|
||||
try {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/graphql`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
}),
|
||||
})
|
||||
|
||||
const { data, errors } = await res.json()
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message)
|
||||
}
|
||||
|
||||
if (res.ok && data) {
|
||||
return data
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,157 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { User } from '../../payload-types'
|
||||
|
||||
type Login = (args: { email: string; password: string }) => Promise<void> // eslint-disable-line no-unused-vars
|
||||
|
||||
type Logout = () => Promise<void>
|
||||
|
||||
type AuthContext = {
|
||||
user?: User | null
|
||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
||||
logout: Logout
|
||||
login: Login
|
||||
}
|
||||
import { gql, USER } from './gql'
|
||||
import { rest } from './rest'
|
||||
import { AuthContext, Create, ForgotPassword, Login, Logout, ResetPassword } from './types'
|
||||
|
||||
const Context = createContext({} as AuthContext)
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode; api?: 'rest' | 'gql' }> = ({
|
||||
children,
|
||||
api = 'rest',
|
||||
}) => {
|
||||
const [user, setUser] = useState<User | null>()
|
||||
|
||||
const login = useCallback<Login>(async args => {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/login`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(args),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
const create = useCallback<Create>(
|
||||
async args => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
setUser(json.user)
|
||||
} else {
|
||||
throw new Error('Invalid login')
|
||||
}
|
||||
}, [])
|
||||
if (api === 'gql') {
|
||||
const { createUser: user } = await gql(`mutation {
|
||||
createUser(data: { email: "${args.email}", password: "${args.password}", firstName: "${args.firstName}", lastName: "${args.lastName}" }) {
|
||||
${USER}
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const login = useCallback<Login>(
|
||||
async args => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/login`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { loginUser } = await gql(`mutation {
|
||||
loginUser(email: "${args.email}", password: "${args.password}") {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
exp
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(loginUser?.user)
|
||||
return loginUser?.user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const logout = useCallback<Logout>(async () => {
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/logout`, {
|
||||
method: 'POST',
|
||||
// Make sure to include cookies with fetch
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
if (api === 'rest') {
|
||||
await rest(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/logout`)
|
||||
setUser(null)
|
||||
} else {
|
||||
throw new Error('There was a problem while logging out.')
|
||||
return
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (api === 'gql') {
|
||||
await gql(`mutation {
|
||||
logoutUser
|
||||
}`)
|
||||
|
||||
setUser(null)
|
||||
}
|
||||
}, [api])
|
||||
|
||||
// On mount, get user and set
|
||||
useEffect(() => {
|
||||
const fetchMe = async () => {
|
||||
const result = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`, {
|
||||
// Make sure to include cookies with fetch
|
||||
credentials: 'include',
|
||||
}).then(req => req.json())
|
||||
setUser(result.user || null)
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/me`,
|
||||
{},
|
||||
{
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
setUser(user)
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { meUser } = await gql(`query {
|
||||
meUser {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
exp
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(meUser.user)
|
||||
}
|
||||
}
|
||||
|
||||
fetchMe()
|
||||
}, [])
|
||||
}, [api])
|
||||
|
||||
const forgotPassword = useCallback<ForgotPassword>(
|
||||
async args => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(
|
||||
`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/forgot-password`,
|
||||
args,
|
||||
)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { forgotPasswordUser } = await gql(`mutation {
|
||||
forgotPasswordUser(email: "${args.email}")
|
||||
}`)
|
||||
|
||||
return forgotPasswordUser
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
const resetPassword = useCallback<ResetPassword>(
|
||||
async args => {
|
||||
if (api === 'rest') {
|
||||
const user = await rest(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/reset-password`, args)
|
||||
setUser(user)
|
||||
return user
|
||||
}
|
||||
|
||||
if (api === 'gql') {
|
||||
const { resetPasswordUser } = await gql(`mutation {
|
||||
resetPasswordUser(password: "${args.password}", token: "${args.token}") {
|
||||
user {
|
||||
${USER}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
|
||||
setUser(resetPasswordUser.user)
|
||||
return resetPasswordUser.user
|
||||
}
|
||||
},
|
||||
[api],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider
|
||||
@@ -70,6 +160,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
||||
setUser,
|
||||
login,
|
||||
logout,
|
||||
create,
|
||||
resetPassword,
|
||||
forgotPassword,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
34
examples/auth/nextjs/components/Auth/rest.ts
Normal file
34
examples/auth/nextjs/components/Auth/rest.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
export const rest = async (
|
||||
url: string,
|
||||
args?: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
options?: RequestInit,
|
||||
): Promise<User | null> => {
|
||||
const method = options?.method || 'POST'
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
...(method === 'POST' ? { body: JSON.stringify(args) } : {}),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
...options,
|
||||
})
|
||||
|
||||
const { errors, user } = await res.json()
|
||||
|
||||
if (errors) {
|
||||
throw new Error(errors[0].message)
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
return user
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
throw new Error(e as string)
|
||||
}
|
||||
}
|
||||
31
examples/auth/nextjs/components/Auth/types.ts
Normal file
31
examples/auth/nextjs/components/Auth/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { User } from '../../payload-types'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export type ResetPassword = (args: {
|
||||
password: string
|
||||
passwordConfirm: string
|
||||
token: string
|
||||
}) => Promise<User>
|
||||
|
||||
export type ForgotPassword = (args: { email: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Create = (args: {
|
||||
email: string
|
||||
password: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
}) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Login = (args: { email: string; password: string }) => Promise<User> // eslint-disable-line no-unused-vars
|
||||
|
||||
export type Logout = () => Promise<void>
|
||||
|
||||
export interface AuthContext {
|
||||
user?: User | null
|
||||
setUser: (user: User | null) => void // eslint-disable-line no-unused-vars
|
||||
logout: Logout
|
||||
login: Login
|
||||
create: Create
|
||||
resetPassword: ResetPassword
|
||||
forgotPassword: ForgotPassword
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
import React from 'react';
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form';
|
||||
import React from 'react'
|
||||
import { FieldValues, UseFormRegister } from 'react-hook-form'
|
||||
|
||||
import classes from './index.module.css';
|
||||
import classes from './index.module.css'
|
||||
|
||||
type Props = {
|
||||
name: string;
|
||||
label: string;
|
||||
register: UseFormRegister<FieldValues & any>;
|
||||
required?: boolean;
|
||||
error: any;
|
||||
type?: 'text' | 'number' | 'password';
|
||||
};
|
||||
name: string
|
||||
label: string
|
||||
register: UseFormRegister<FieldValues & any>
|
||||
required?: boolean
|
||||
error: any
|
||||
type?: 'text' | 'number' | 'password'
|
||||
}
|
||||
|
||||
export const Input: React.FC<Props> = ({ name, label, required, register, error, type = 'text' }) => {
|
||||
export const Input: React.FC<Props> = ({
|
||||
name,
|
||||
label,
|
||||
required,
|
||||
register,
|
||||
error,
|
||||
type = 'text',
|
||||
}) => {
|
||||
return (
|
||||
<div className={classes.input}>
|
||||
<label htmlFor="name" className={classes.label}>
|
||||
@@ -21,5 +28,5 @@ export const Input: React.FC<Props> = ({ name, label, required, register, error,
|
||||
<input {...{ type }} {...register(name, { required })} />
|
||||
{error && <div className={classes.error}>This field is required</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import '../css/app.scss'
|
||||
|
||||
export default function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
// The `AuthProvider` can be used with either REST or GraphQL APIs
|
||||
// Just change the `api` prop to "graphql" or "rest", that's it!
|
||||
<AuthProvider api="rest">
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</AuthProvider>
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
}
|
||||
|
||||
.success,
|
||||
.error {
|
||||
margin-bottom: 15px;
|
||||
.error,
|
||||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success {
|
||||
@@ -13,4 +14,4 @@
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ const Account: React.FC = () => {
|
||||
return (
|
||||
<Gutter>
|
||||
<h1>Account</h1>
|
||||
{router.query.message && <div className={classes.message}>{router.query.message}</div>}
|
||||
{error && <div className={classes.error}>{error}</div>}
|
||||
{success && <div className={classes.success}>{success}</div>}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className={classes.form}>
|
||||
|
||||
@@ -4,4 +4,5 @@
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type FormData = {
|
||||
const CreateAccount: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { login } = useAuth()
|
||||
const { login, create, user } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -27,28 +27,16 @@ const CreateAccount: React.FC = () => {
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Automatically log the user in
|
||||
try {
|
||||
await create(data as Parameters<typeof create>[0])
|
||||
// Automatically log the user in after creating their account
|
||||
await login({ email: data.email, password: data.password })
|
||||
|
||||
// Set success message for user
|
||||
setSuccess(true)
|
||||
|
||||
// Clear any existing errors
|
||||
setError('')
|
||||
} else {
|
||||
setError('There was a problem creating your account. Please try again later.')
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to create your account.')
|
||||
}
|
||||
},
|
||||
[login],
|
||||
[login, create],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -16,14 +16,14 @@ const Home: React.FC = () => {
|
||||
</Link>
|
||||
{". This example demonstrates how to implement Payload's "}
|
||||
<Link href="https://payloadcms.com/docs/authentication/overview">Authentication</Link>
|
||||
{' strategies.'}
|
||||
{' strategies in both the REST and GraphQL APIs.'}
|
||||
</p>
|
||||
<p>
|
||||
{'Visit the '}
|
||||
<Link href="/login">Login</Link>
|
||||
{' page to start the authentication flow. Once logged in, you will be redirected to the '}
|
||||
<Link href="/account">Account</Link>
|
||||
{" page which is restricted to user's only."}
|
||||
{` page which is restricted to user's only. To toggle APIs, simply toggle the "api" prop in _app.tsx between "rest" and "gql".`}
|
||||
</p>
|
||||
</Gutter>
|
||||
)
|
||||
|
||||
@@ -29,8 +29,8 @@ const Login: React.FC = () => {
|
||||
try {
|
||||
await login(data)
|
||||
router.push('/account')
|
||||
} catch (_) {
|
||||
setError('There was an error with the credentials provided. Please try again.')
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to login.')
|
||||
}
|
||||
},
|
||||
[login, router],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ const Logout: React.FC = () => {
|
||||
try {
|
||||
await logout()
|
||||
setSuccess('Logged out successfully.')
|
||||
} catch (_) {
|
||||
setError('You are already logged out.')
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to logout.')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { useAuth } from '../../components/Auth'
|
||||
import { Gutter } from '../../components/Gutter'
|
||||
import { Input } from '../../components/Input'
|
||||
import classes from './index.module.css'
|
||||
@@ -12,6 +13,7 @@ type FormData = {
|
||||
const RecoverPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const [success, setSuccess] = useState(false)
|
||||
const { forgotPassword } = useAuth()
|
||||
|
||||
const {
|
||||
register,
|
||||
@@ -19,27 +21,21 @@ const RecoverPassword: React.FC = () => {
|
||||
formState: { errors },
|
||||
} = useForm<FormData>()
|
||||
|
||||
const onSubmit = useCallback(async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/forgot-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
try {
|
||||
const user = await forgotPassword(data as Parameters<typeof forgotPassword>[0])
|
||||
|
||||
if (response.ok) {
|
||||
// Set success message for user
|
||||
setSuccess(true)
|
||||
|
||||
// Clear any existing errors
|
||||
setError('')
|
||||
} else {
|
||||
setError(
|
||||
'There was a problem while attempting to send you a password reset email. Please try again later.',
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
if (user) {
|
||||
setSuccess(true)
|
||||
setError('')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to recover password.')
|
||||
}
|
||||
},
|
||||
[forgotPassword],
|
||||
)
|
||||
|
||||
return (
|
||||
<Gutter>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.error {
|
||||
color: red;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type FormData = {
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const [error, setError] = useState('')
|
||||
const { login } = useAuth()
|
||||
const { login, resetPassword } = useAuth()
|
||||
const router = useRouter()
|
||||
|
||||
const token = typeof router.query.token === 'string' ? router.query.token : undefined
|
||||
@@ -28,31 +28,23 @@ const ResetPassword: React.FC = () => {
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: FormData) => {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/users/reset-password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
try {
|
||||
const user = await resetPassword(data as Parameters<typeof resetPassword>[0])
|
||||
|
||||
if (response.ok) {
|
||||
const json = await response.json()
|
||||
|
||||
// Automatically log the user in after they successfully reset password
|
||||
await login({ email: json.user.email, password: data.password })
|
||||
|
||||
// Redirect them to /account with success message in URL
|
||||
router.push('/account?success=Password reset successfully.')
|
||||
} else {
|
||||
setError('There was a problem while resetting your password. Please try again later.')
|
||||
if (user) {
|
||||
// Automatically log the user in after they successfully reset password
|
||||
// Then redirect them to /account with success message in URL
|
||||
await login({ email: user.email, password: data.password })
|
||||
router.push('/account?success=Password reset successfully.')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err?.message || 'An error occurred while attempting to reset password.')
|
||||
}
|
||||
},
|
||||
[router, login],
|
||||
[router, login, resetPassword],
|
||||
)
|
||||
|
||||
// when NextJS populates token within router,
|
||||
// reset form with new token value
|
||||
// When Next.js populates token within router, reset form with new token value
|
||||
useEffect(() => {
|
||||
reset({ token })
|
||||
}, [reset, token])
|
||||
|
||||
Reference in New Issue
Block a user