feat(auth): draft auth implementation

This commit is contained in:
Bolaji Ayodeji 2021-07-14 11:52:43 +01:00 committed by Alessandro Casazza
parent 4adba68c4c
commit 4b27c4849b
No known key found for this signature in database
GPG Key ID: 3AF41B06C6495D3D
25 changed files with 290 additions and 39 deletions

View File

@ -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<CommercelayerAPI, 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,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

View File

@ -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<CommercelayerAPI, 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,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

View File

@ -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<CommerceAPIConfig, 'fetch'> {
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.`
)
}

View File

@ -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'
}
}

View File

@ -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,
})
}

View File

@ -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<typeof handler>
export const handler: MutationHook<any> = {
export const handler: MutationHook<LoginHook> = {
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]
)
},
}

View File

@ -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<typeof handler>
export const handler: MutationHook<any> = {
export const handler: MutationHook<SignupHook> = {
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 }) => () => {
const { revalidate } = useCustomer()
return useCallback(
async function signup(input) {
const data = await fetch({ input })
await revalidate()
return data
},
[fetch, revalidate]
)
},
useHook:
({ fetch }) =>
() =>
() => {},
}

View File

@ -1,9 +1,9 @@
{
"provider": "local",
"provider": "commercelayer",
"features": {
"wishlist": false,
"cart": false,
"search": false,
"customerAuth": false
"customerAuth": true
}
}

View File

@ -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<CommerceConfig>) {
return (
<CoreCommerceProvider
provider={localProvider}
config={{ ...localConfig, ...config }}
provider={commerceLayerProvider}
config={{ ...commerceLayerConfig, ...config }}
>
{children}
</CoreCommerceProvider>

View File

@ -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,

View File

@ -0,0 +1 @@
export * from '@commerce/types/cart'

View File

@ -0,0 +1 @@
export * from '@commerce/types/checkout'

View File

@ -0,0 +1 @@
export * from '@commerce/types/common'

View File

@ -0,0 +1 @@
export * from '@commerce/types/customer'

View File

@ -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,
}

View File

@ -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<T extends LoginTypes = LoginTypes> = {
data: null
actionInput: LoginBody
fetcherInput: LoginBody
body: T['body']
}

View File

@ -0,0 +1 @@
export * from '@commerce/types/logout'

View File

@ -0,0 +1 @@
export * from '@commerce/types/page'

View File

@ -0,0 +1 @@
export * from '@commerce/types/product'

View File

@ -0,0 +1 @@
export * from '@commerce/types/signup'

View File

@ -0,0 +1 @@
export * from '@commerce/types/site'

View File

@ -0,0 +1 @@
export * from '@commerce/types/wishlist'

View File

@ -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"],