login and signup features

This commit is contained in:
Alan 2021-06-30 12:47:15 -03:00
parent 716b540966
commit cf742e8fd5
22 changed files with 394 additions and 32 deletions

View File

@ -39,9 +39,7 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
<Searchbar /> <Searchbar />
</div> </div>
<div className="flex justify-end flex-1 space-x-8"> <div className="flex justify-end flex-1 space-x-8">{<UserNav />}</div>
{/* <UserNav /> */}
</div>
</div> </div>
<div className="flex pb-4 lg:px-6 lg:hidden"> <div className="flex pb-4 lg:px-6 lg:hidden">

View File

@ -28,15 +28,15 @@ const LINKS = [
{ {
name: 'My Profile', name: 'My Profile',
href: '/profile', href: '/profile',
}, } /*,
{ {
name: 'My Cart', name: 'My Cart',
href: '/cart', href: '/cart',
}, },*/,
] ]
const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => { const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
const logout = useLogout() //const logout = useLogout()
const { pathname } = useRouter() const { pathname } = useRouter()
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const [display, setDisplay] = useState(false) const [display, setDisplay] = useState(false)
@ -107,14 +107,14 @@ const DropdownMenu: FC<DropdownMenuProps> = ({ open = false }) => {
</div> </div>
</a> </a>
</li> </li>
<li> {/*<li>
<a <a
className={cn(s.link, 'border-t border-accents-2 mt-4')} className={cn(s.link, 'border-t border-accents-2 mt-4')}
onClick={() => logout()} onClick={() => logout()}
> >
Logout Logout
</a> </a>
</li> </li>*/}
</ul> </ul>
)} )}
</div> </div>

View File

@ -17,20 +17,20 @@ interface Props {
const countItem = (count: number, item: LineItem) => count + item.quantity const countItem = (count: number, item: LineItem) => count + item.quantity
const UserNav: FC<Props> = ({ className }) => { const UserNav: FC<Props> = ({ className }) => {
const { data } = useCart() //const { data } = useCart()
const { data: customer } = useCustomer() const { data: customer } = useCustomer()
const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI() const { toggleSidebar, closeSidebarIfPresent, openModal } = useUI()
const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0 //const itemsCount = data?.lineItems.reduce(countItem, 0) ?? 0
return ( return (
<nav className={cn(s.root, className)}> <nav className={cn(s.root, className)}>
<div className={s.mainContainer}> <div className={s.mainContainer}>
<ul className={s.list}> <ul className={s.list}>
<li className={s.item} onClick={toggleSidebar}> {/*<li className={s.item} onClick={toggleSidebar}>
<Bag /> <Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>} {itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</li> </li>
{process.env.COMMERCE_WISHLIST_ENABLED && ( process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}> <li className={s.item}>
<Link href="/wishlist"> <Link href="/wishlist">
<a onClick={closeSidebarIfPresent} aria-label="Wishlist"> <a onClick={closeSidebarIfPresent} aria-label="Wishlist">
@ -38,7 +38,7 @@ const UserNav: FC<Props> = ({ className }) => {
</a> </a>
</Link> </Link>
</li> </li>
)} )*/}
<li className={s.item}> <li className={s.item}>
{customer ? ( {customer ? (
<DropdownMenu /> <DropdownMenu />

View File

@ -0,0 +1,39 @@
import type { CustomerEndpoint } from '.'
import getCustomerQuery from '../../../utils/queries/get-customer-query'
const jwt = require('jwt-simple')
export const getLoggedInCustomerQuery = getCustomerQuery
export type Customer = NonNullable<any['customer']>
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
req,
res,
config,
}) => {
const { customerCookie } = config
// customerId
if (req.cookies[config.customerCookie]) {
const id = jwt.decode(req.cookies[config.customerCookie], customerCookie)
if (id) {
const { data } = await config.fetch<any>(getCustomerQuery, {
variables: { id },
})
const { customer } = data
if (!customer) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
})
}
return res.status(200).json({ data: { customer } })
}
}
res.status(200).json({ data: null })
}
export default getLoggedInCustomer

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerEndpoint from '@commerce/api/endpoints/customer'
import type { CustomerSchema } from '../../../types/customer'
import type { CommercetoolsAPI } from '../..'
import getLoggedInCustomer from './get-logged-in-customer'
export type CustomerAPI = GetAPISchema<CommercetoolsAPI, CustomerSchema>
export type CustomerEndpoint = CustomerAPI['endpoint']
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
const customerApi = createEndpoint<CustomerAPI>({
handler: customerEndpoint,
handlers,
})
export default customerApi

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import loginEndpoint from '@commerce/api/endpoints/login'
import type { LoginSchema } from '../../../types/login'
import type { CommercetoolsAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<CommercetoolsAPI, LoginSchema>
export type LoginEndpoint = LoginAPI['endpoint']
export const handlers: LoginEndpoint['handlers'] = { login }
const loginApi = createEndpoint<LoginAPI>({
handler: loginEndpoint,
handlers,
})
export default loginApi

View File

@ -0,0 +1,48 @@
import { FetcherError } from '@commerce/utils/errors'
import type { LoginEndpoint } from '.'
const invalidCredentials = /invalid credentials/i
const login: LoginEndpoint['handlers']['login'] = async ({
res,
body: { email, password },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
try {
await commerce.login({
variables: { data: { email, password } },
config,
res,
})
} catch (error) {
if (
error instanceof FetcherError &&
invalidCredentials.test(error.message)
) {
return res.status(401).json({
data: null,
errors: [
{
message:
'Cannot find an account that matches the provided credentials',
code: 'invalid_credentials',
},
],
})
}
throw error
}
res.status(200).json({ data: null })
}
export default login

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@commerce/api'
import signupEndpoint from '@commerce/api/endpoints/signup'
import type { SignupSchema } from '../../../types/signup'
import type { CommercetoolsAPI } from '../..'
import signup from './signup'
export type SignupAPI = GetAPISchema<CommercetoolsAPI, SignupSchema>
export type SignupEndpoint = SignupAPI['endpoint']
export const handlers: SignupEndpoint['handlers'] = { signup }
const singupApi = createEndpoint<SignupAPI>({
handler: signupEndpoint,
handlers,
})
export default singupApi

View File

@ -0,0 +1,48 @@
import type { SignupEndpoint } from '.'
import type { CommercetoolsCustomer } from '../../../types/customer'
import { signupMutation } from '../../../utils/mutations/sign-up-mutation'
const jwt = require('jwt-simple')
import { serialize } from 'cookie'
const signup: SignupEndpoint['handlers']['signup'] = async ({
res,
body: { firstName, lastName, email, password },
config,
}) => {
if (!(firstName && lastName && email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
try {
const newCustomer: CommercetoolsCustomer = (
await config.fetch(signupMutation, {
variables: { data: { firstName, lastName, email, password } },
})
).data?.customerSignUp.customer
const { customerCookie } = config
const customerExpires = new Date(Date.now() + 30 * 30) // 1 month
const customerToken = jwt.encode(newCustomer.id, customerCookie)
res.setHeader(
'Set-Cookie',
serialize(customerCookie, customerToken, {
maxAge: 30,
path: '/',
expires: customerExpires,
})
)
res.status(200).json({ data: null })
} catch (error) {
return res.status(400).json({
data: null,
errors: [{ message: 'Error on response' }],
})
}
}
export default signup

View File

@ -16,6 +16,9 @@ import getAllPages from '@framework/api/operations/get-all-pages'
import login from '@framework/api/operations/login' import login from '@framework/api/operations/login'
import getCustomerWishlist from '@framework/api/operations/get-customer-wishlist' import getCustomerWishlist from '@framework/api/operations/get-customer-wishlist'
import getSiteInfo from '@framework/api/operations/get-site-info' import getSiteInfo from '@framework/api/operations/get-site-info'
import type { LoginAPI } from './endpoints/login'
import type { CustomerAPI } from './endpoints/customer'
import type { SignupAPI } from './endpoints/signup'
export interface CommercetoolsConfig extends CommerceAPIConfig { export interface CommercetoolsConfig extends CommerceAPIConfig {
locale: string locale: string
@ -39,6 +42,7 @@ const CLIENT_SECRET = process.env.CTP_CLIENT_SECRET || 'projectKey'
const AUTH_URL = process.env.CTP_AUTH_URL || 'projectKey' const AUTH_URL = process.env.CTP_AUTH_URL || 'projectKey'
const API_URL = process.env.CTP_API_URL || 'projectKey' const API_URL = process.env.CTP_API_URL || 'projectKey'
const CONCURRENCY = process.env.CTP_CONCURRENCY || 0 const CONCURRENCY = process.env.CTP_CONCURRENCY || 0
const CUSTOMER_COOKIE_NAME = process.env.CTP_CUSTOMER_COOKIE || 'projectKey'
if (!API_URL) { if (!API_URL) {
throw new Error( throw new Error(
@ -72,7 +76,7 @@ const config: CommercetoolsConfig = {
apiToken: '', apiToken: '',
cartCookie: '', cartCookie: '',
cartCookieMaxAge: 0, cartCookieMaxAge: 0,
customerCookie: '', customerCookie: CUSTOMER_COOKIE_NAME,
fetch: fetchGraphql, fetch: fetchGraphql,
fetchProducts: fetchProducts, fetchProducts: fetchProducts,
} }
@ -88,6 +92,8 @@ const operations = {
login, login,
} }
export type APIs = LoginAPI | CustomerAPI | SignupAPI
export const provider = { config, operations } export const provider = { config, operations }
export type Provider = typeof provider export type Provider = typeof provider

View File

@ -3,18 +3,22 @@ import type {
OperationContext, OperationContext,
OperationOptions, OperationOptions,
} from '@commerce/api/operations' } from '@commerce/api/operations'
import { Provider, CommercetoolsConfig } from '@framework/api' import type { LoginOperation } from '../../types/login'
import { Provider, CommercetoolsConfig } from '..'
import { loginMutation } from '../../utils/mutations/log-in-mutation'
import { serialize } from 'cookie'
const jwt = require('jwt-simple')
export default function loginOperation({ export default function loginOperation({
commerce, commerce,
}: OperationContext<Provider>) { }: OperationContext<Provider>) {
async function login<T extends { variables: any; data: any }>(opts: { async function login<T extends LoginOperation>(opts: {
variables: T['variables'] variables: T['variables']
config?: Partial<CommercetoolsConfig> config?: Partial<CommercetoolsConfig>
res: ServerResponse res: ServerResponse
}): Promise<T['data']> }): Promise<T['data']>
async function login<T extends { variables: any; data: any }>( async function login<T extends LoginOperation>(
opts: { opts: {
variables: T['variables'] variables: T['variables']
config?: Partial<CommercetoolsConfig> config?: Partial<CommercetoolsConfig>
@ -22,11 +26,11 @@ export default function loginOperation({
} & OperationOptions } & OperationOptions
): Promise<T['data']> ): Promise<T['data']>
async function login<T extends { variables: any; data: any }>({ async function login<T extends LoginOperation>({
query = '', query = loginMutation,
variables, variables,
res: response,
config: cfg, config: cfg,
res: response,
}: { }: {
query?: string query?: string
variables: T['variables'] variables: T['variables']
@ -34,10 +38,29 @@ export default function loginOperation({
config?: Partial<CommercetoolsConfig> config?: Partial<CommercetoolsConfig>
}): Promise<T['data']> { }): Promise<T['data']> {
const config = commerce.getConfig(cfg) const config = commerce.getConfig(cfg)
return { const expireTime = new Date(Date.now() + 30 * 30) // 1 month
result: '',
}
}
const { data } = await config.fetch<any>(query, {
variables,
})
if (data) {
const customerToken = jwt.encode(
data.customerSignIn.customer.id,
config.customerCookie
)
response.setHeader('Set-Cookie', [
serialize(config.customerCookie, customerToken, {
maxAge: 30,
path: '/',
expires: expireTime,
}),
])
return { result: data.customerSignIn.customer.id }
} else {
return {}
}
}
return login return login
} }

View File

@ -1,3 +1,3 @@
// export { default as useLogin } from './use-login' export { default as useLogin } from './use-login'
// export { default as useLogout } from './use-logout' export { default as useLogout } from './use-logout'
// export { default as useSignup } from './use-signup' export { default as useSignup } from './use-signup'

View File

@ -0,0 +1,36 @@
import { useCallback } from 'react'
import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login'
import { CommerceError } from '@commerce/utils/errors'
import useCustomer from '../customer/use-customer'
export default useLogin as UseLogin<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/login',
method: 'POST',
},
async fetcher({ input: { email, password }, options, fetch }) {
if (!(email && password)) {
throw new CommerceError({
message: 'An email and password are required to login',
})
}
return fetch({
...options,
body: { email, password },
})
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
return useCallback(
async function login(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

View File

@ -0,0 +1,44 @@
import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import { CommerceError } from '@commerce/utils/errors'
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
import type { SignupHook } from '../types/signup'
import useCustomer from '../customer/use-customer'
export default useSignup as UseSignup<typeof handler>
export const handler: MutationHook<SignupHook> = {
fetchOptions: {
url: '/api/signup',
method: 'POST',
},
async fetcher({
input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
return fetch({
...options,
body: { firstName, lastName, email, password },
})
},
useHook: ({ fetch }) => () => {
const { revalidate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
}

View File

@ -1 +1 @@
// export { default as useCustomer } from './use-customer' export { default as useCustomer } from './use-customer'

View File

@ -0,0 +1,22 @@
import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<CustomerHook> = {
fetchOptions: {
url: '/api/customer',
method: 'GET',
},
async fetcher({ options, fetch }) {
const data = await fetch(options)
return data?.customer ?? null
},
useHook: ({ useData }) => (input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
},
}

View File

@ -3,11 +3,11 @@ import { Provider } from '@commerce'
// import { handler as useAddItem } from './cart/use-add-item' // import { handler as useAddItem } from './cart/use-add-item'
// import { handler as useUpdateItem } from './cart/use-update-item' // import { handler as useUpdateItem } from './cart/use-update-item'
// import { handler as useRemoveItem } from './cart/use-remove-item' // import { handler as useRemoveItem } from './cart/use-remove-item'
// import { handler as useCustomer } from './customer/use-customer' import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search' import { handler as useSearch } from './product/use-search'
// import { handler as useLogin } from './auth/use-login' import { handler as useLogin } from './auth/use-login'
// import { handler as useLogout } from './auth/use-logout' // import { handler as useLogout } from './auth/use-logout'
// import { handler as useSignup } from './auth/use-signup' import { handler as useSignup } from './auth/use-signup'
import fetcher from './fetcher' import fetcher from './fetcher'
// Export a provider with the CommerceHooks // Export a provider with the CommerceHooks
@ -16,9 +16,9 @@ export const commercetoolsProvider: Provider = {
cartCookie: 'session', cartCookie: 'session',
fetcher, fetcher,
// cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, // cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
// customer: { useCustomer }, customer: { useCustomer },
products: { useSearch }, products: { useSearch },
// auth: { useLogin, useLogout, useSignup } auth: { useLogin, useSignup }, // useLogout
} }
export type CommercetoolsProvider = typeof commercetoolsProvider export type CommercetoolsProvider = typeof commercetoolsProvider

View File

@ -0,0 +1,17 @@
/* GraphQL */
export const loginMutation = `
mutation customerSignInDraft($data: CustomerSignInDraft!) {
customerSignIn(draft: $data) {
customer {
id
email
firstName
lastName
password
}
cart{
id
}
}
}
`

View File

@ -0,0 +1,10 @@
/* GraphQL */
export const signupMutation = `
mutation createCustomer($data: CustomerSignUpDraft!) {
customerSignUp (draft: $data) {
customer {
id
}
}
}
`

View File

@ -0,0 +1,11 @@
/* GraphQL */
const getCustomerQuery = `query ($id: String!) {
customer(id: $id) {
id
firstName
lastName
email
}
}`
export default getCustomerQuery

View File

@ -35,6 +35,7 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"js-cookie": "^2.2.1", "js-cookie": "^2.2.1",
"jwt-simple": "^0.5.6",
"keen-slider": "^5.2.4", "keen-slider": "^5.2.4",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.random": "^3.2.0", "lodash.random": "^3.2.0",

View File

@ -3827,6 +3827,11 @@ jws@^3.2.2:
jwa "^1.4.1" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jwt-simple@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a"
integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==
keen-slider@^5.2.4: keen-slider@^5.2.4:
version "5.4.0" version "5.4.0"
resolved "https://registry.yarnpkg.com/keen-slider/-/keen-slider-5.4.0.tgz#e5a949e2bb237d7d6b068458dcda59ae9c415254" resolved "https://registry.yarnpkg.com/keen-slider/-/keen-slider-5.4.0.tgz#e5a949e2bb237d7d6b068458dcda59ae9c415254"