From 4b27c4849b91819cfbcac5e4501ed3af8d8c274c Mon Sep 17 00:00:00 2001 From: Bolaji Ayodeji Date: Wed, 14 Jul 2021 11:52:43 +0100 Subject: [PATCH] feat(auth): draft auth implementation --- .../api/endpoints/login/index.ts | 19 ++++++- .../api/endpoints/login/login.ts | 47 ++++++++++++++++ .../api/endpoints/signup/index.ts | 19 ++++++- .../api/endpoints/signup/signup.ts | 55 +++++++++++++++++++ framework/commercelayer/api/index.ts | 7 +-- framework/commercelayer/api/utils/errors.ts | 22 ++++++++ .../commercelayer/api/utils/fetch-api.ts | 7 +-- framework/commercelayer/auth/use-login.tsx | 40 +++++++++++--- framework/commercelayer/auth/use-signup.tsx | 47 ++++++++++++---- framework/commercelayer/commerce.config.json | 4 +- framework/commercelayer/index.tsx | 8 +-- framework/commercelayer/provider.ts | 4 +- framework/commercelayer/types/cart.ts | 1 + framework/commercelayer/types/checkout.ts | 1 + framework/commercelayer/types/common.ts | 1 + framework/commercelayer/types/customer.ts | 1 + framework/commercelayer/types/index.ts | 25 +++++++++ framework/commercelayer/types/login.ts | 11 ++++ framework/commercelayer/types/logout.ts | 1 + framework/commercelayer/types/page.ts | 1 + framework/commercelayer/types/product.ts | 1 + framework/commercelayer/types/signup.ts | 1 + framework/commercelayer/types/site.ts | 1 + framework/commercelayer/types/wishlist.ts | 1 + tsconfig.json | 4 +- 25 files changed, 290 insertions(+), 39 deletions(-) create mode 100644 framework/commercelayer/api/endpoints/login/login.ts create mode 100644 framework/commercelayer/api/endpoints/signup/signup.ts create mode 100644 framework/commercelayer/api/utils/errors.ts create mode 100644 framework/commercelayer/types/cart.ts create mode 100644 framework/commercelayer/types/checkout.ts create mode 100644 framework/commercelayer/types/common.ts create mode 100644 framework/commercelayer/types/customer.ts create mode 100644 framework/commercelayer/types/index.ts create mode 100644 framework/commercelayer/types/login.ts create mode 100644 framework/commercelayer/types/logout.ts create mode 100644 framework/commercelayer/types/page.ts create mode 100644 framework/commercelayer/types/product.ts create mode 100644 framework/commercelayer/types/signup.ts create mode 100644 framework/commercelayer/types/site.ts create mode 100644 framework/commercelayer/types/wishlist.ts diff --git a/framework/commercelayer/api/endpoints/login/index.ts b/framework/commercelayer/api/endpoints/login/index.ts index 491bf0ac9..86aa11793 100644 --- a/framework/commercelayer/api/endpoints/login/index.ts +++ b/framework/commercelayer/api/endpoints/login/index.ts @@ -1 +1,18 @@ -export default function noopApi(...args: any[]): void {} +import { GetAPISchema, createEndpoint } from '@commerce/api' +import loginEndpoint from '@commerce/api/endpoints/login' +import type { LoginSchema } from '../../../types/login' +import type { CommercelayerAPI } from '../..' +import login from './login' + +export type LoginAPI = GetAPISchema + +export type LoginEndpoint = LoginAPI['endpoint'] + +export const handlers: LoginEndpoint['handlers'] = { login } + +const loginApi = createEndpoint({ + handler: loginEndpoint, + handlers, +}) + +export default loginApi \ No newline at end of file diff --git a/framework/commercelayer/api/endpoints/login/login.ts b/framework/commercelayer/api/endpoints/login/login.ts new file mode 100644 index 000000000..61cd12109 --- /dev/null +++ b/framework/commercelayer/api/endpoints/login/login.ts @@ -0,0 +1,47 @@ +import { FetcherError } from '@commerce/utils/errors' +import { getAccessToken } from '../../index' +import type { LoginEndpoint } from '.' + +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 { + const user = { email, password }; + const getToken = await getAccessToken(user); + await config.apiFetch(`/api/customers/${getToken.customerId}`, { + method: 'GET' + }) + } catch (error) { + if (error instanceof FetcherError && + /invalid credentials/i.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 + } + + await commerce.login({ variables: { email, password }, config, res }) + + res.status(200).json({ data: null }) +} + +export default login \ No newline at end of file diff --git a/framework/commercelayer/api/endpoints/signup/index.ts b/framework/commercelayer/api/endpoints/signup/index.ts index 491bf0ac9..3d43b121e 100644 --- a/framework/commercelayer/api/endpoints/signup/index.ts +++ b/framework/commercelayer/api/endpoints/signup/index.ts @@ -1 +1,18 @@ -export default function noopApi(...args: any[]): void {} +import { GetAPISchema, createEndpoint } from '@commerce/api' +import signupEndpoint from '@commerce/api/endpoints/signup' +import type { SignupSchema } from '../../../types/signup' +import type { CommercelayerAPI } from '../..' +import signup from './signup' + +export type SignupAPI = GetAPISchema + +export type SignupEndpoint = SignupAPI['endpoint'] + +export const handlers: SignupEndpoint['handlers'] = { signup } + +const singupApi = createEndpoint({ + handler: signupEndpoint, + handlers, +}) + +export default singupApi diff --git a/framework/commercelayer/api/endpoints/signup/signup.ts b/framework/commercelayer/api/endpoints/signup/signup.ts new file mode 100644 index 000000000..204379a30 --- /dev/null +++ b/framework/commercelayer/api/endpoints/signup/signup.ts @@ -0,0 +1,55 @@ +import { CommercelayerApiError } from '../../utils/errors' +import type { SignupEndpoint } from '.' + +const signup: SignupEndpoint['handlers']['signup'] = async ({ + res, + body: { email, password }, + config, + commerce, +}) => { + if (!(email && password)) { + return res.status(400).json({ + data: null, + errors: [{ message: 'Invalid request' }], + }) + } + + try { + await config.apiFetch('/api/customers', { + method: 'POST', + body: { + data: { + type: 'customers', + attributes: { + email: email, + password: password, + }, + }, + }, + }) + } catch (error) { + if (error instanceof CommercelayerApiError && error.status === 422) { + const inputEmail = error.data?.errors.meta.value + + if ('code' in error.data?.errors) { + return res.status(400).json({ + data: null, + errors: [ + { + message: `A user already exists with ${inputEmail}`, + code: 'USER_EXISTS', + }, + ], + }) + } + } + + throw error + } + + await commerce.login({ variables: { email, password }, res, config }) + + res.status(200).json({ data: null }) +} + +export default signup diff --git a/framework/commercelayer/api/index.ts b/framework/commercelayer/api/index.ts index 8b7a47d76..2af8db558 100644 --- a/framework/commercelayer/api/index.ts +++ b/framework/commercelayer/api/index.ts @@ -12,7 +12,6 @@ import getAllProducts from './operations/get-all-products' import getProduct from './operations/get-product' import { getToken } from './utils/get-token' -import fetch from './utils/fetch' export interface CommercelayerConfig extends Omit { apiClientId: string @@ -36,19 +35,19 @@ const MARKET_SCOPE = process.env.COMMERCELAYER_MARKET_SCOPE if (!CLIENT_ID) { throw new Error( - `The environment variable COMMERCELAYER_CLIENT_ID is missing and it's required to access your store` + `The environment variable COMMERCELAYER_CLIENT_ID is missing and it's required.` ) } if (!ENDPOINT) { throw new Error( - `The environment variable COMMERCELAYER_ENDPOINT is missing and it's required to access your store` + `The environment variable COMMERCELAYER_ENDPOINT is missing and it's required.` ) } if (!MARKET_SCOPE) { throw new Error( - `The environment variable COMMERCELAYER_MARKET_SCOPE is missing and it's required to access your store` + `The environment variable COMMERCELAYER_MARKET_SCOPE is missing and it's required.` ) } diff --git a/framework/commercelayer/api/utils/errors.ts b/framework/commercelayer/api/utils/errors.ts new file mode 100644 index 000000000..6acc9fd0d --- /dev/null +++ b/framework/commercelayer/api/utils/errors.ts @@ -0,0 +1,22 @@ +import type { Response } from '@vercel/fetch' + +export class CommercelayerApiError extends Error { + status: number + res: Response + data: any + + constructor(msg: string, res: Response, data?: any) { + super(msg) + this.name = 'CommercelayerApiError' + this.status = res.status + this.res = res + this.data = data + } +} + +export class CommercelayerNetworkError extends Error { + constructor(msg: string) { + super(msg) + this.name = 'CommercelayerNetworkError' + } +} \ No newline at end of file diff --git a/framework/commercelayer/api/utils/fetch-api.ts b/framework/commercelayer/api/utils/fetch-api.ts index 85ca08c1b..6764feaf5 100644 --- a/framework/commercelayer/api/utils/fetch-api.ts +++ b/framework/commercelayer/api/utils/fetch-api.ts @@ -16,21 +16,20 @@ const fetchApi = const token = getToken?.accessToken const res = await fetch(config.commerceUrl + endpoint, { ...fetchOptions, - method: 'POST', headers: { ...fetchOptions?.headers, 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, - body: JSON.stringify({ + body: JSON.stringify([ query, - }), + ]), }) const json = await res.json() if (json.errors) { throw new FetcherError({ - errors: json.errors ?? [{ message: 'Failed to fetch for API' }], + errors: json.errors ?? [{ message: 'Failed to fetch Commerce Layer API' }], status: res.status, }) } diff --git a/framework/commercelayer/auth/use-login.tsx b/framework/commercelayer/auth/use-login.tsx index 28351dc7f..aa74689b9 100644 --- a/framework/commercelayer/auth/use-login.tsx +++ b/framework/commercelayer/auth/use-login.tsx @@ -1,16 +1,40 @@ -import { MutationHook } from '@commerce/utils/types' +import { useCallback } from 'react' +import type { MutationHook } from '@commerce/utils/types' +import { CommerceError } from '@commerce/utils/errors' import useLogin, { UseLogin } from '@commerce/auth/use-login' +import type { LoginHook } from '../types/login' +import useCustomer from '../customer/use-customer' export default useLogin as UseLogin -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - query: '', + url: '/api/customers', + method: 'GET', }, - async fetcher() { - return null + async fetcher({ input: { email, password }, options, fetch }) { + if (!(email && password)) { + throw new CommerceError({ + message: + 'An email address and password are required to login', + }) + } + + return fetch({ + ...options, + body: { email, password }, + }) }, - useHook: () => () => { - return async function () {} + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function login(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) }, -} +} \ No newline at end of file diff --git a/framework/commercelayer/auth/use-signup.tsx b/framework/commercelayer/auth/use-signup.tsx index e9ad13458..d1cfbbe9a 100644 --- a/framework/commercelayer/auth/use-signup.tsx +++ b/framework/commercelayer/auth/use-signup.tsx @@ -1,19 +1,44 @@ import { useCallback } from 'react' -import useCustomer from '../customer/use-customer' -import { MutationHook } from '@commerce/utils/types' +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 -export const handler: MutationHook = { +export const handler: MutationHook = { fetchOptions: { - query: '', + url: '/api/customers', + method: 'POST', }, - async fetcher() { - return null + async fetcher({ + input: { email, password }, + options, + fetch, + }) { + if (!(email && password)) { + throw new CommerceError({ + message: + 'An email address and password are required to signup', + }) + } + + return fetch({ + ...options, + body: { email, password }, + }) }, - useHook: - ({ fetch }) => - () => - () => {}, -} + useHook: ({ fetch }) => () => { + const { revalidate } = useCustomer() + + return useCallback( + async function signup(input) { + const data = await fetch({ input }) + await revalidate() + return data + }, + [fetch, revalidate] + ) + }, +} \ No newline at end of file diff --git a/framework/commercelayer/commerce.config.json b/framework/commercelayer/commerce.config.json index 261211527..460e52b3a 100644 --- a/framework/commercelayer/commerce.config.json +++ b/framework/commercelayer/commerce.config.json @@ -1,9 +1,9 @@ { - "provider": "local", + "provider": "commercelayer", "features": { "wishlist": false, "cart": false, "search": false, - "customerAuth": false + "customerAuth": true } } diff --git a/framework/commercelayer/index.tsx b/framework/commercelayer/index.tsx index 2ec304f63..9c614577f 100644 --- a/framework/commercelayer/index.tsx +++ b/framework/commercelayer/index.tsx @@ -1,13 +1,13 @@ import * as React from 'react' import { ReactNode } from 'react' -import { localProvider } from './provider' +import { commerceLayerProvider } from './provider' import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce, } from '@commerce' -export const localConfig: CommerceConfig = { +export const commerceLayerConfig: CommerceConfig = { locale: 'en-us', cartCookie: 'session', } @@ -21,8 +21,8 @@ export function CommerceProvider({ } & Partial) { return ( {children} diff --git a/framework/commercelayer/provider.ts b/framework/commercelayer/provider.ts index e6a2b0a21..f5e9e55c4 100644 --- a/framework/commercelayer/provider.ts +++ b/framework/commercelayer/provider.ts @@ -9,8 +9,8 @@ import { handler as useLogin } from './auth/use-login' import { handler as useLogout } from './auth/use-logout' import { handler as useSignup } from './auth/use-signup' -export type Provider = typeof localProvider -export const localProvider = { +export type Provider = typeof commerceLayerProvider +export const commerceLayerProvider = { locale: 'en-us', cartCookie: 'session', fetcher: fetcher, diff --git a/framework/commercelayer/types/cart.ts b/framework/commercelayer/types/cart.ts new file mode 100644 index 000000000..6ed5c6c64 --- /dev/null +++ b/framework/commercelayer/types/cart.ts @@ -0,0 +1 @@ +export * from '@commerce/types/cart' diff --git a/framework/commercelayer/types/checkout.ts b/framework/commercelayer/types/checkout.ts new file mode 100644 index 000000000..4e2412ef6 --- /dev/null +++ b/framework/commercelayer/types/checkout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/checkout' diff --git a/framework/commercelayer/types/common.ts b/framework/commercelayer/types/common.ts new file mode 100644 index 000000000..b52c33a4d --- /dev/null +++ b/framework/commercelayer/types/common.ts @@ -0,0 +1 @@ +export * from '@commerce/types/common' diff --git a/framework/commercelayer/types/customer.ts b/framework/commercelayer/types/customer.ts new file mode 100644 index 000000000..87c9afcc4 --- /dev/null +++ b/framework/commercelayer/types/customer.ts @@ -0,0 +1 @@ +export * from '@commerce/types/customer' diff --git a/framework/commercelayer/types/index.ts b/framework/commercelayer/types/index.ts new file mode 100644 index 000000000..7ab0b7f64 --- /dev/null +++ b/framework/commercelayer/types/index.ts @@ -0,0 +1,25 @@ +import * as Cart from './cart' +import * as Checkout from './checkout' +import * as Common from './common' +import * as Customer from './customer' +import * as Login from './login' +import * as Logout from './logout' +import * as Page from './page' +import * as Product from './product' +import * as Signup from './signup' +import * as Site from './site' +import * as Wishlist from './wishlist' + +export type { + Cart, + Checkout, + Common, + Customer, + Login, + Logout, + Page, + Product, + Signup, + Site, + Wishlist, +} diff --git a/framework/commercelayer/types/login.ts b/framework/commercelayer/types/login.ts new file mode 100644 index 000000000..ab11a420a --- /dev/null +++ b/framework/commercelayer/types/login.ts @@ -0,0 +1,11 @@ +import * as Core from '@commerce/types/login' +import { LoginBody, LoginTypes } from '@commerce/types/login' + +export * from '@commerce/types/login' + +export type LoginHook = { + data: null + actionInput: LoginBody + fetcherInput: LoginBody + body: T['body'] +} diff --git a/framework/commercelayer/types/logout.ts b/framework/commercelayer/types/logout.ts new file mode 100644 index 000000000..9f0a466af --- /dev/null +++ b/framework/commercelayer/types/logout.ts @@ -0,0 +1 @@ +export * from '@commerce/types/logout' diff --git a/framework/commercelayer/types/page.ts b/framework/commercelayer/types/page.ts new file mode 100644 index 000000000..20ec8ea38 --- /dev/null +++ b/framework/commercelayer/types/page.ts @@ -0,0 +1 @@ +export * from '@commerce/types/page' diff --git a/framework/commercelayer/types/product.ts b/framework/commercelayer/types/product.ts new file mode 100644 index 000000000..c776d58fa --- /dev/null +++ b/framework/commercelayer/types/product.ts @@ -0,0 +1 @@ +export * from '@commerce/types/product' diff --git a/framework/commercelayer/types/signup.ts b/framework/commercelayer/types/signup.ts new file mode 100644 index 000000000..58543c6f6 --- /dev/null +++ b/framework/commercelayer/types/signup.ts @@ -0,0 +1 @@ +export * from '@commerce/types/signup' diff --git a/framework/commercelayer/types/site.ts b/framework/commercelayer/types/site.ts new file mode 100644 index 000000000..bfef69cf9 --- /dev/null +++ b/framework/commercelayer/types/site.ts @@ -0,0 +1 @@ +export * from '@commerce/types/site' diff --git a/framework/commercelayer/types/wishlist.ts b/framework/commercelayer/types/wishlist.ts new file mode 100644 index 000000000..8907fbf82 --- /dev/null +++ b/framework/commercelayer/types/wishlist.ts @@ -0,0 +1 @@ +export * from '@commerce/types/wishlist' diff --git a/tsconfig.json b/tsconfig.json index db71b765b..51c174bd6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,8 +24,8 @@ "@components/*": ["components/*"], "@commerce": ["framework/commerce"], "@commerce/*": ["framework/commerce/*"], - "@framework": ["framework/local"], - "@framework/*": ["framework/local/*"] + "@framework": ["framework/commercelayer"], + "@framework/*": ["framework/commercelayer/*"] } }, "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],