Add ordercloud login handling

This commit is contained in:
Adam Clason 2023-01-22 15:56:16 -06:00
parent b18973dfb3
commit e9b47f27a5
7 changed files with 155 additions and 31 deletions

View File

@ -4,19 +4,21 @@ import createEndpoints from '@vercel/commerce/api/endpoints'
import cart from './cart' import cart from './cart'
import checkout from './checkout' import checkout from './checkout'
import login from './login'
import logout from './logout'
import signup from './signup'
import products from './catalog/products' import products from './catalog/products'
import customer from './customer' import customer from './customer'
import customerCard from './customer/card' import customerCard from './customer/card'
import customerAddress from './customer/address' import customerAddress from './customer/address'
import signup from './signup'
import logout from './logout'
const endpoints = { const endpoints = {
cart, cart,
checkout, checkout,
logout: logout, login,
signup: signup, logout,
customer: customer, signup,
customer,
'customer/card': customerCard, 'customer/card': customerCard,
'customer/address': customerAddress, 'customer/address': customerAddress,
'catalog/products': products, 'catalog/products': products,

View File

@ -0,0 +1,18 @@
import { GetAPISchema, createEndpoint } from '@vercel/commerce/api'
import loginEndpoint from '@vercel/commerce/api/endpoints/login'
import type { LoginSchema } from '@vercel/commerce/types/login'
import type { OrdercloudAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<OrdercloudAPI, 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,54 @@
import type { LoginEndpoint } from '.'
import { FetcherError } from '@vercel/commerce/utils/errors'
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
import { getFetchConfig, getToken } from '../..//utils/fetch-rest'
import { serialize } from 'cookie'
const invalidCredentials = /Invalid username or password/i
const login: LoginEndpoint['handlers']['login'] = async ({
body: { email, password },
config,
}) => {
try {
const token = await getToken({
grantType: 'password',
username: email,
password: password,
...getFetchConfig(config),
})
if (!token.access_token) {
throw new CommerceAPIError('Failed to retrieve access token', {
status: 401,
})
}
return {
headers: {
'Set-Cookie': serialize(config.tokenCookie, token.access_token, {
expires: new Date(Date.now() + token.expires_in * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
},
data: null,
}
} catch (error) {
// Check if the email and password didn't match an existing account
if (error instanceof FetcherError) {
throw new CommerceAPIError(
error.errors.some((e) => invalidCredentials.test(e.message))
? 'Cannot find an account that matches the provided credentials'
: error.message,
{ status: error.status || 401 }
)
} else {
throw error
}
}
}
export default login

View File

@ -1,31 +1,39 @@
import { FetcherError } from '@vercel/commerce/utils/errors' import { FetcherError } from '@vercel/commerce/utils/errors'
import { OrdercloudConfig } from '../index' import { OrdercloudConfig } from '../index'
export let token: string | null = null export type GetTokenParams = (
| {
// Get token util grantType: 'password'
async function getToken({ username: string
baseUrl, password: string
clientId, }
clientSecret, | { grantType: 'client_credentials' }
}: { ) & {
baseUrl: string baseUrl: string
clientId: string clientId: string
clientSecret?: string clientSecret?: string
}): Promise<{ }
// Get token util
export async function getToken(params: GetTokenParams): Promise<{
access_token: string access_token: string
expires_in: number expires_in: number
refresh_token: string refresh_token: string
token_type: string token_type: string
}> { }> {
let body = `client_id=${params.clientId}&client_secret=${params.clientSecret}&grant_type=${params.grantType}`
if (params.grantType === 'password') {
body += `&username=${params.username}&password=${params.password}`
}
// If not, get a new one and store it // If not, get a new one and store it
const authResponse = await fetch(`${baseUrl}/oauth/token`, { const authResponse = await fetch(`${params.baseUrl}/oauth/token`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json', Accept: 'application/json',
}, },
body: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`, body,
}) })
// If something failed getting the auth response // If something failed getting the auth response
@ -37,8 +45,18 @@ async function getToken({
// And return an error // And return an error
throw new FetcherError({ throw new FetcherError({
errors: [{ message: error.error_description.Code }], errors: [
status: error.error_description.HttpStatus, {
message:
typeof error.error_description === 'object'
? error.error_description.Code
: error.error_description,
},
],
status:
typeof error.error_description === 'object'
? error.error_description.HttpStatus
: undefined,
}) })
} }
@ -102,6 +120,11 @@ export async function fetchData<T>(opts: {
} }
} }
export const getFetchConfig = (config: OrdercloudConfig) => ({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string,
})
export const createBuyerFetcher: ( export const createBuyerFetcher: (
getConfig: () => OrdercloudConfig getConfig: () => OrdercloudConfig
) => <T>( ) => <T>(
@ -117,6 +140,8 @@ export const createBuyerFetcher: (
body?: Record<string, unknown>, body?: Record<string, unknown>,
fetchOptions?: Record<string, any> fetchOptions?: Record<string, any>
) => { ) => {
let token: string | null = null
if (fetchOptions?.token) { if (fetchOptions?.token) {
token = fetchOptions?.token token = fetchOptions?.token
} }
@ -128,8 +153,8 @@ export const createBuyerFetcher: (
if (!token) { if (!token) {
const newToken = await getToken({ const newToken = await getToken({
baseUrl: config.commerceUrl, grantType: 'client_credentials',
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string, ...getFetchConfig(config),
}) })
token = newToken.access_token token = newToken.access_token
meta.token = newToken meta.token = newToken

View File

@ -1,17 +1,16 @@
import { Fetcher } from '@vercel/commerce/utils/types' import { Fetcher } from '@vercel/commerce/utils/types'
import { handleFetchResponse } from './utils'
const clientFetcher: Fetcher = async ({ method, url, body }) => { const clientFetcher: Fetcher = async ({ method, url, body }) => {
const response = await fetch(url!, { return handleFetchResponse(
method, await fetch(url!, {
body: body ? JSON.stringify(body) : undefined, method,
headers: { body: body ? JSON.stringify(body) : undefined,
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json',
}) },
.then((response) => response.json()) })
.then((response) => response.data) )
return response
} }
export default clientFetcher export default clientFetcher

View File

@ -0,0 +1,25 @@
import { FetcherError } from '@vercel/commerce/utils/errors'
export function getError(errors: any[] | null, status: number) {
errors = errors ?? [{ message: 'Failed to fetch OrderCloud API' }]
return new FetcherError({ errors, status })
}
export async function getAsyncError(res: Response) {
const data = await res.json()
return getError(data.errors, res.status)
}
export const handleFetchResponse = async (res: Response) => {
if (res.ok) {
const { data, errors } = await res.json()
if (errors && errors.length) {
throw getError(errors, res.status)
}
return data
}
throw await getAsyncError(res)
}

View File

@ -0,0 +1 @@
export * from './handle-fetch-response'