mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 06:56:59 +00:00
Add ordercloud login handling
This commit is contained in:
parent
b18973dfb3
commit
e9b47f27a5
@ -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,
|
||||
|
18
packages/ordercloud/src/api/endpoints/login/index.ts
Normal file
18
packages/ordercloud/src/api/endpoints/login/index.ts
Normal 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
|
54
packages/ordercloud/src/api/endpoints/login/login.ts
Normal file
54
packages/ordercloud/src/api/endpoints/login/login.ts
Normal 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
|
@ -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
|
||||
|
@ -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!, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => response.data)
|
||||
|
||||
return response
|
||||
return handleFetchResponse(
|
||||
await fetch(url!, {
|
||||
method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export default clientFetcher
|
||||
|
25
packages/ordercloud/src/utils/handle-fetch-response.ts
Normal file
25
packages/ordercloud/src/utils/handle-fetch-response.ts
Normal 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)
|
||||
}
|
1
packages/ordercloud/src/utils/index.ts
Normal file
1
packages/ordercloud/src/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './handle-fetch-response'
|
Loading…
x
Reference in New Issue
Block a user