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