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 checkout from './checkout'
import login from './login'
import logout from './logout'
import signup from './signup'
import products from './catalog/products'
import customer from './customer'
import customerCard from './customer/card'
import customerAddress from './customer/address'
import signup from './signup'
import logout from './logout'
const endpoints = {
cart,
checkout,
logout: logout,
signup: signup,
customer: customer,
login,
logout,
signup,
customer,
'customer/card': customerCard,
'customer/address': customerAddress,
'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 { OrdercloudConfig } from '../index'
export let token: string | null = null
// Get token util
async function getToken({
baseUrl,
clientId,
clientSecret,
}: {
export type GetTokenParams = (
| {
grantType: 'password'
username: string
password: string
}
| { grantType: 'client_credentials' }
) & {
baseUrl: string
clientId: string
clientSecret?: string
}): Promise<{
}
// Get token util
export async function getToken(params: GetTokenParams): Promise<{
access_token: string
expires_in: number
refresh_token: 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
const authResponse = await fetch(`${baseUrl}/oauth/token`, {
const authResponse = await fetch(`${params.baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: `client_id=${clientId}&client_secret=${clientSecret}&grant_type=client_credentials`,
body,
})
// If something failed getting the auth response
@ -37,8 +45,18 @@ async function getToken({
// And return an error
throw new FetcherError({
errors: [{ message: error.error_description.Code }],
status: error.error_description.HttpStatus,
errors: [
{
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: (
getConfig: () => OrdercloudConfig
) => <T>(
@ -117,6 +140,8 @@ export const createBuyerFetcher: (
body?: Record<string, unknown>,
fetchOptions?: Record<string, any>
) => {
let token: string | null = null
if (fetchOptions?.token) {
token = fetchOptions?.token
}
@ -128,8 +153,8 @@ export const createBuyerFetcher: (
if (!token) {
const newToken = await getToken({
baseUrl: config.commerceUrl,
clientId: process.env.ORDERCLOUD_BUYER_CLIENT_ID as string,
grantType: 'client_credentials',
...getFetchConfig(config),
})
token = newToken.access_token
meta.token = newToken

View File

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