mirror of
https://github.com/vercel/commerce.git
synced 2025-05-17 15:06:59 +00:00
Fix switchable runtimes
This commit is contained in:
parent
dae40fd7f1
commit
8873d6fa5d
@ -53,7 +53,9 @@
|
||||
"cookie": "^0.4.1",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"uuidv4": "^6.2.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12",
|
||||
@ -65,6 +67,7 @@
|
||||
"@taskr/esnext": "^1.1.0",
|
||||
"@taskr/watch": "^1.1.0",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/jsonwebtoken": "^8.5.7",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
|
@ -1,8 +1,5 @@
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import jwt from '@tsndr/cloudflare-worker-jwt'
|
||||
import { uuid } from '@cfworker/uuid'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const fullCheckout = true
|
||||
|
||||
@ -24,6 +21,7 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
|
||||
@ -33,6 +31,17 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
return { redirectTo: data.checkout_url }
|
||||
}
|
||||
} else {
|
||||
// Dynamically import uuid & jsonwebtoken based on the runtime
|
||||
const { uuid } =
|
||||
process.env.NEXT_RUNTIME === 'edge'
|
||||
? await import('@cfworker/uuid')
|
||||
: await import('uuidv4')
|
||||
|
||||
const jwt =
|
||||
process.env.NEXT_RUNTIME === 'edge'
|
||||
? await import('@tsndr/cloudflare-worker-jwt')
|
||||
: await import('jsonwebtoken')
|
||||
|
||||
const dateCreated = Math.round(new Date().getTime() / 1000)
|
||||
const payload = {
|
||||
iss: config.storeApiClientId,
|
||||
@ -80,7 +89,7 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
</html>
|
||||
`
|
||||
|
||||
return new NextResponse(html, {
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
},
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { LoginEndpoint } from '.'
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||
|
||||
@ -12,7 +11,7 @@ const login: LoginEndpoint['handlers']['login'] = async ({
|
||||
commerce,
|
||||
}) => {
|
||||
try {
|
||||
const res = new NextResponse(null)
|
||||
const res = new Response()
|
||||
await commerce.login({ variables: { email, password }, config, res })
|
||||
return {
|
||||
status: res.status,
|
||||
|
@ -1,6 +1,4 @@
|
||||
import type { SignupEndpoint } from '.'
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { CommerceAPIError } from '@vercel/commerce/api/utils/errors'
|
||||
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
@ -39,7 +37,7 @@ const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const res = new NextResponse()
|
||||
const res = new Response()
|
||||
|
||||
// Login the customer right after creating it
|
||||
await commerce.login({ variables: { email, password }, res, config })
|
||||
|
@ -7,7 +7,6 @@ import type { LoginMutation } from '../../../schema'
|
||||
import type { RecursivePartial } from '../utils/types'
|
||||
import concatHeader from '../utils/concat-cookie'
|
||||
import type { BigcommerceConfig, Provider } from '..'
|
||||
import type { NextResponse } from 'next/server'
|
||||
|
||||
export const loginMutation = /* GraphQL */ `
|
||||
mutation login($email: String!, $password: String!) {
|
||||
@ -23,14 +22,14 @@ export default function loginOperation({
|
||||
async function login<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
res: NextResponse
|
||||
res: Response
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function login<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: BigcommerceConfig
|
||||
res: NextResponse
|
||||
res: Response
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
@ -42,7 +41,7 @@ export default function loginOperation({
|
||||
}: {
|
||||
query?: string
|
||||
variables: T['variables']
|
||||
res: NextResponse
|
||||
res: Response
|
||||
config?: BigcommerceConfig
|
||||
}): Promise<T['data']> {
|
||||
config = commerce.getConfig(config)
|
||||
|
@ -1,11 +1,9 @@
|
||||
import type { GetAPISchema } from '..'
|
||||
import type { CartSchema } from '../../types/cart'
|
||||
|
||||
import parse from '../utils/parse-output'
|
||||
import { parse, getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
|
||||
import {
|
||||
getCartBodySchema,
|
||||
addItemBodySchema,
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
searchProductBodySchema,
|
||||
searchProductsSchema,
|
||||
} from '../../../schemas/product'
|
||||
import parse from '../../utils/parse-output'
|
||||
import { parse } from '../../utils'
|
||||
|
||||
const productsEndpoint: GetAPISchema<
|
||||
any,
|
||||
@ -27,8 +27,7 @@ const productsEndpoint: GetAPISchema<
|
||||
const res = await handlers['getProducts']({ ...ctx, body })
|
||||
|
||||
res.headers = {
|
||||
'Cache-Control':
|
||||
'max-age=0, s-maxage=3600, stale-while-revalidate=60, public',
|
||||
'Cache-Control': 'max-age=0, s-maxage=3600, stale-while-revalidate, public',
|
||||
...res.headers,
|
||||
}
|
||||
|
||||
|
@ -7,10 +7,8 @@ import {
|
||||
submitCheckoutBodySchema,
|
||||
} from '../../schemas/checkout'
|
||||
|
||||
import { parse, getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
import parse from '../utils/parse-output'
|
||||
import { z } from 'zod'
|
||||
import { getInput } from '../utils'
|
||||
|
||||
const checkoutEndpoint: GetAPISchema<
|
||||
any,
|
||||
@ -31,7 +29,7 @@ const checkoutEndpoint: GetAPISchema<
|
||||
if (req.method === 'GET') {
|
||||
const body = getCheckoutBodySchema.parse({ ...input, cartId })
|
||||
const res = await handlers['getCheckout']({ ...ctx, body })
|
||||
return parse(res, checkoutSchema.optional().or(z.string()))
|
||||
return parse(res, checkoutSchema.optional())
|
||||
}
|
||||
|
||||
// Create checkout
|
||||
|
@ -1,7 +1,6 @@
|
||||
import type { CustomerAddressSchema } from '../../../types/customer/address'
|
||||
import type { GetAPISchema } from '../..'
|
||||
|
||||
import parse from '../../utils/parse-output'
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
import {
|
||||
@ -11,7 +10,7 @@ import {
|
||||
updateAddressBodySchema,
|
||||
} from '../../../schemas/customer'
|
||||
|
||||
import { getInput } from '../../utils'
|
||||
import { parse, getInput } from '../../utils'
|
||||
import { getCartBodySchema } from '../../../schemas/cart'
|
||||
|
||||
// create a function that returns a function
|
||||
|
@ -3,16 +3,15 @@ import type { GetAPISchema } from '../..'
|
||||
|
||||
import { z } from 'zod'
|
||||
|
||||
import parse from '../../utils/parse-output'
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
import {
|
||||
cardSchema,
|
||||
addCardBodySchema,
|
||||
deleteCardBodySchema,
|
||||
updateCardBodySchema,
|
||||
} from '../../../schemas/customer'
|
||||
import { getInput } from '../../utils'
|
||||
import { parse, getInput } from '../../utils'
|
||||
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
const customerCardEndpoint: GetAPISchema<
|
||||
any,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type { CustomerSchema } from '../../../types/customer'
|
||||
import type { GetAPISchema } from '../..'
|
||||
|
||||
import parse from '../../utils/parse-output'
|
||||
import { parse } from '../../utils'
|
||||
import validateHandlers from '../../utils/validate-handlers'
|
||||
|
||||
import { customerSchema } from '../../../schemas/customer'
|
||||
|
@ -1,7 +1,5 @@
|
||||
import type { APIProvider, CommerceAPI, EndpointHandler } from '..'
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { normalizeApiError } from '../utils/errors'
|
||||
import edgeHandler from '../utils/edge-handler'
|
||||
import nodeHandler from '../utils/node-handler'
|
||||
|
||||
/**
|
||||
* Next.js Commerce API endpoints handler. Based on the path, it will call the corresponding endpoint handler,
|
||||
@ -9,72 +7,4 @@ import { normalizeApiError } from '../utils/errors'
|
||||
* @param {CommerceAPI} commerce The Commerce API instance.
|
||||
* @param endpoints An object containing the handlers for each endpoint.
|
||||
*/
|
||||
export default function createEndpoints<P extends APIProvider>(
|
||||
commerce: CommerceAPI<P>,
|
||||
endpoints: Record<string, (commerce: CommerceAPI<P>) => EndpointHandler>
|
||||
) {
|
||||
const endpointsKeys = Object.keys(endpoints)
|
||||
const handlers = endpointsKeys.reduce<Record<string, EndpointHandler>>(
|
||||
(acc, endpoint) =>
|
||||
Object.assign(acc, {
|
||||
[endpoint]: endpoints[endpoint](commerce),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
||||
return async (req: NextRequest) => {
|
||||
try {
|
||||
const { pathname } = new URL(req.url)
|
||||
|
||||
/**
|
||||
* Get the current endpoint by removing the leading and trailing slash & base path.
|
||||
* Csovers: /api/commerce/cart & /checkout
|
||||
*/
|
||||
const endpoint = pathname
|
||||
.replace('/api/commerce/', '')
|
||||
.replace(/^\/|\/$/g, '')
|
||||
|
||||
// Check if the handler for this path exists and return a 404 if it doesn't
|
||||
if (!endpointsKeys.includes(endpoint)) {
|
||||
throw new Error(
|
||||
`Endpoint "${endpoint}" not implemented. Please use one of the available api endpoints: ${endpointsKeys.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the handler for this endpoint, provided by the provider,
|
||||
* parses the input body and returns the parsed output
|
||||
*/
|
||||
const output = await handlers[endpoint](req)
|
||||
|
||||
// If the output is a NextResponse, return it directly (E.g. checkout page & validateMethod util)
|
||||
if (output instanceof NextResponse) {
|
||||
return output
|
||||
}
|
||||
|
||||
// If the output contains a redirectTo property, return a NextResponse with the redirect
|
||||
if (output.redirectTo) {
|
||||
return NextResponse.redirect(output.redirectTo, {
|
||||
headers: output.headers,
|
||||
})
|
||||
}
|
||||
|
||||
const { data = null, errors, status, headers } = output
|
||||
|
||||
return NextResponse.json(
|
||||
{ data, errors },
|
||||
{
|
||||
status,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
const output = normalizeApiError(error)
|
||||
return output instanceof NextResponse
|
||||
? output
|
||||
: NextResponse.json(output, { status: output.status ?? 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
export default process.env.NEXT_RUNTIME === 'edge' ? edgeHandler : nodeHandler
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { GetAPISchema } from '..'
|
||||
import type { LoginSchema } from '../../types/login'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
import { loginBodySchema } from '../../schemas/auth'
|
||||
|
||||
const loginEndpoint: GetAPISchema<
|
||||
@ -16,9 +16,10 @@ const loginEndpoint: GetAPISchema<
|
||||
POST: handlers['login'],
|
||||
GET: handlers['login'],
|
||||
})
|
||||
|
||||
const input = await getInput(req)
|
||||
const body = loginBodySchema.parse(input)
|
||||
return await handlers['login']({ ...ctx, body })
|
||||
return handlers['login']({ ...ctx, body })
|
||||
}
|
||||
|
||||
export default loginEndpoint
|
||||
|
@ -3,7 +3,6 @@ import type { LogoutSchema } from '../../types/logout'
|
||||
|
||||
import { logoutBodySchema } from '../../schemas/auth'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
import { normalizeApiError } from '../utils/errors'
|
||||
|
||||
const logoutEndpoint: GetAPISchema<
|
||||
any,
|
||||
@ -21,7 +20,7 @@ const logoutEndpoint: GetAPISchema<
|
||||
typeof redirectTo === 'string' ? { redirectTo } : {}
|
||||
)
|
||||
|
||||
return await handlers['logout']({ ...ctx, body })
|
||||
return handlers['logout']({ ...ctx, body })
|
||||
}
|
||||
|
||||
export default logoutEndpoint
|
||||
|
@ -1,9 +1,9 @@
|
||||
import type { GetAPISchema } from '..'
|
||||
import type { SignupSchema } from '../../types/signup'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
import { signupBodySchema } from '../../schemas/auth'
|
||||
|
||||
const signupEndpoint: GetAPISchema<
|
||||
@ -21,7 +21,7 @@ const signupEndpoint: GetAPISchema<
|
||||
const cartId = cookies.get(config.cartCookie)
|
||||
|
||||
const body = signupBodySchema.parse({ ...input, cartId })
|
||||
return await handlers['signup']({ ...ctx, body })
|
||||
return handlers['signup']({ ...ctx, body })
|
||||
}
|
||||
|
||||
export default signupEndpoint
|
||||
|
@ -1,9 +1,8 @@
|
||||
import type { GetAPISchema } from '..'
|
||||
import type { WishlistSchema } from '../../types/wishlist'
|
||||
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
import { parse, getInput } from '../utils'
|
||||
|
||||
import { getInput } from '../utils'
|
||||
import {
|
||||
wishlistSchema,
|
||||
addItemBodySchema,
|
||||
@ -11,7 +10,7 @@ import {
|
||||
getWishlistBodySchema,
|
||||
} from '../../schemas/whishlist'
|
||||
|
||||
import parse from '../utils/parse-output'
|
||||
import validateHandlers from '../utils/validate-handlers'
|
||||
|
||||
const wishlistEndpoint: GetAPISchema<
|
||||
any,
|
||||
@ -53,7 +52,7 @@ const wishlistEndpoint: GetAPISchema<
|
||||
output = await handlers['removeItem']({ ...ctx, body })
|
||||
}
|
||||
|
||||
return output ? parse(output, wishlistSchema.optional()) : { status: 40 }
|
||||
return output ? parse(output, wishlistSchema.optional()) : { status: 405 }
|
||||
}
|
||||
|
||||
export default wishlistEndpoint
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import type { APIEndpoint, APIHandler, APIResponse } from './utils/types'
|
||||
import type { CartSchema } from '../types/cart'
|
||||
import type { CustomerSchema } from '../types/customer'
|
||||
@ -12,7 +11,6 @@ import type { CheckoutSchema } from '../types/checkout'
|
||||
import type { CustomerCardSchema } from '../types/customer/card'
|
||||
import type { CustomerAddressSchema } from '../types/customer/address'
|
||||
|
||||
import { geolocation } from '@vercel/edge'
|
||||
import { withOperationCallback } from './utils/with-operation-callback'
|
||||
|
||||
import {
|
||||
@ -140,7 +138,6 @@ export function getEndpoint<
|
||||
config: cfg,
|
||||
handlers: context.handlers,
|
||||
options: context.options ?? {},
|
||||
geolocation: geolocation(req),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import type {
|
||||
GetProductOperation,
|
||||
} from '../types/product'
|
||||
import type { APIProvider, CommerceAPI } from '.'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const noop = () => {
|
||||
throw new Error('Not implemented')
|
||||
@ -44,14 +43,14 @@ export type Operations<P extends APIProvider> = {
|
||||
<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: P['config']
|
||||
res: NextResponse
|
||||
res: Response
|
||||
}): Promise<T['data']>
|
||||
|
||||
<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: P['config']
|
||||
res: NextResponse
|
||||
res: Response
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
}
|
||||
|
82
packages/commerce/src/api/utils/edge-handler.ts
Normal file
82
packages/commerce/src/api/utils/edge-handler.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import type { APIProvider, CommerceAPI, EndpointHandler } from '..'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import { normalizeApiError } from './errors'
|
||||
|
||||
export default function edgeApi<P extends APIProvider>(
|
||||
commerce: CommerceAPI<P>,
|
||||
endpoints: Record<string, (commerce: CommerceAPI<P>) => EndpointHandler>
|
||||
) {
|
||||
const endpointsKeys = Object.keys(endpoints)
|
||||
|
||||
const handlers = endpointsKeys.reduce<Record<string, EndpointHandler>>(
|
||||
(acc, endpoint) =>
|
||||
Object.assign(acc, {
|
||||
[endpoint]: endpoints[endpoint](commerce),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
||||
return async (req: NextRequest) => {
|
||||
try {
|
||||
const { pathname } = new URL(req.url)
|
||||
|
||||
/**
|
||||
* Get the current endpoint by removing the leading and trailing slash & base path.
|
||||
* Csovers: /api/commerce/cart & /checkout
|
||||
*/
|
||||
const endpoint = pathname
|
||||
.replace('/api/commerce/', '')
|
||||
.replace(/^\/|\/$/g, '')
|
||||
|
||||
// Check if the handler for this path exists and return a 404 if it doesn't
|
||||
if (!endpointsKeys.includes(endpoint)) {
|
||||
throw new Error(
|
||||
`Endpoint "${endpoint}" not implemented. Please use one of the available api endpoints: ${endpointsKeys.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the handler for this endpoint, provided by the provider,
|
||||
* parses the input body and returns the parsed output
|
||||
*/
|
||||
const output = await handlers[endpoint](req)
|
||||
|
||||
// If the output is a Response, return it directly (E.g. checkout page & validateMethod util)
|
||||
if (output instanceof Response) {
|
||||
return output
|
||||
}
|
||||
|
||||
const { headers } = output
|
||||
|
||||
// If the output contains a redirectTo property, return a Response with the redirect
|
||||
if (output.redirectTo) {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
...headers,
|
||||
Location: output.redirectTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Otherwise, return a JSON response with the output data or errors returned by the handler
|
||||
const { data = null, errors, status } = output
|
||||
return new Response(JSON.stringify({ data, errors }), {
|
||||
status,
|
||||
headers,
|
||||
})
|
||||
} catch (error) {
|
||||
const output = normalizeApiError(error)
|
||||
if (output instanceof Response) {
|
||||
return output
|
||||
}
|
||||
const { status = 500, ...rest } = output
|
||||
return output instanceof Response
|
||||
? output
|
||||
: new Response(JSON.stringify(rest), { status })
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,14 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import { CommerceError } from '../../utils/errors'
|
||||
import { ZodError } from 'zod'
|
||||
|
||||
export class CommerceAPIResponseError extends Error {
|
||||
status: number
|
||||
res: NextResponse
|
||||
res: Response
|
||||
data: any
|
||||
|
||||
constructor(msg: string, res: NextResponse, data?: any) {
|
||||
constructor(msg: string, res: Response, data?: any) {
|
||||
super(msg)
|
||||
this.name = 'CommerceApiError'
|
||||
this.status = res.status
|
||||
|
@ -1,3 +1,52 @@
|
||||
import type { NextApiRequest } from 'next'
|
||||
import type { ZodSchema } from 'zod'
|
||||
import type { APIResponse } from './types'
|
||||
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
/**
|
||||
* Parses the output data of the API handler and returns a valid APIResponse
|
||||
* or throws an error if the data is invalid.
|
||||
* @param res APIResponse
|
||||
* @param parser ZodSchema
|
||||
*/
|
||||
export const parse = <T>(res: APIResponse<T>, parser: ZodSchema) => {
|
||||
if (res.data) {
|
||||
res.data = parser.parse(res.data)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the body of the request as a JSON object.
|
||||
* @param req NextRequest
|
||||
*/
|
||||
export const getInput = (req: NextRequest) => req.json().catch(() => ({}))
|
||||
|
||||
/**
|
||||
* Convert NextApiRequest to NextRequest
|
||||
* @param req NextApiRequest
|
||||
* @param path string
|
||||
*/
|
||||
export const transformRequest = (req: NextApiRequest, path: string) => {
|
||||
let body
|
||||
const headers = new Headers()
|
||||
|
||||
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
||||
headers.append(req.rawHeaders[i], req.rawHeaders[i + 1])
|
||||
}
|
||||
|
||||
if (
|
||||
req.method === 'POST' ||
|
||||
req.method === 'PUT' ||
|
||||
req.method === 'DELETE'
|
||||
) {
|
||||
body = JSON.stringify(req.body)
|
||||
}
|
||||
|
||||
return new NextRequest(`https://${req.headers.host}/api/commerce/${path}`, {
|
||||
headers,
|
||||
method: req.method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
81
packages/commerce/src/api/utils/node-handler.ts
Normal file
81
packages/commerce/src/api/utils/node-handler.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { APIProvider, CommerceAPI, EndpointHandler } from '..'
|
||||
|
||||
import { normalizeApiError } from './errors'
|
||||
import { transformRequest } from '.'
|
||||
|
||||
export default function nodeApi<P extends APIProvider>(
|
||||
commerce: CommerceAPI<P>,
|
||||
endpoints: {
|
||||
[key: string]: (commerce: CommerceAPI<P>) => EndpointHandler
|
||||
}
|
||||
) {
|
||||
const paths = Object.keys(endpoints)
|
||||
|
||||
const handlers = paths.reduce<Record<string, EndpointHandler>>(
|
||||
(acc, path) =>
|
||||
Object.assign(acc, {
|
||||
[path]: endpoints[path](commerce),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
try {
|
||||
if (!req.query.commerce) {
|
||||
throw new Error(
|
||||
'Invalid configuration. Please make sure that the /pages/api/commerce/[[...commerce]].ts route is configured correctly, and it passes the commerce instance.'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url path
|
||||
*/
|
||||
const path = Array.isArray(req.query.commerce)
|
||||
? req.query.commerce.join('/')
|
||||
: req.query.commerce
|
||||
|
||||
// Check if the handler for this path exists and return a 404 if it doesn't
|
||||
if (!paths.includes(path)) {
|
||||
throw new Error(
|
||||
`Endpoint handler not implemented. Please use one of the available api endpoints: ${paths.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
const newReq = transformRequest(req, path)
|
||||
const output = await handlers[path](newReq)
|
||||
|
||||
if (output instanceof Response) {
|
||||
return res.end(output.body)
|
||||
}
|
||||
|
||||
const { status, errors, data, redirectTo, headers } = output
|
||||
|
||||
if (headers) {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
res.setHeader(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if (redirectTo) {
|
||||
return res.redirect(redirectTo)
|
||||
}
|
||||
|
||||
res.status(status || 200).json({
|
||||
data,
|
||||
errors,
|
||||
})
|
||||
} catch (error) {
|
||||
const output = normalizeApiError(error)
|
||||
|
||||
if (output instanceof Response) {
|
||||
return res.end(output.body)
|
||||
}
|
||||
|
||||
const { status = 500, ...rest } = normalizeApiError(error)
|
||||
res.status(status).json(rest)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import type { ZodSchema } from 'zod'
|
||||
import { APIResponse } from './types'
|
||||
|
||||
export const parseOutput = <T>(res: APIResponse<T>, parser: ZodSchema) => {
|
||||
if (res.data) {
|
||||
res.data = parser.parse(res.data)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export default parseOutput
|
@ -1,5 +1,4 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import type { Geo } from '@vercel/edge'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import type { CommerceAPI } from '..'
|
||||
|
||||
export type ErrorData = { message: string; code?: string }
|
||||
@ -27,7 +26,6 @@ export type APIHandlerContext<
|
||||
config: C['provider']['config']
|
||||
handlers: H
|
||||
options: Options
|
||||
geolocation: Geo
|
||||
}
|
||||
|
||||
export type APIHandler<
|
||||
|
@ -1,13 +1,10 @@
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
import type { APIHandler } from './types'
|
||||
import validateMethod, { HTTP_METHODS } from './validate-method'
|
||||
import { APIHandler } from './types'
|
||||
|
||||
/**
|
||||
* Validates the request method and throws an error if it's not allowed, or if the handler is not implemented.
|
||||
* and stops the execution of the handler.
|
||||
* @param req The request object.
|
||||
* @param res The response object.
|
||||
* @param allowedOperations An object containing the handlers for each method.
|
||||
* @throws Error when the method is not allowed or the handler is not implemented.
|
||||
*/
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { CommerceAPIResponseError } from './errors'
|
||||
|
||||
export type HTTP_METHODS = 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
|
||||
export default function isAllowedMethod(
|
||||
export default function validateMethod(
|
||||
req: NextRequest,
|
||||
allowedMethods: HTTP_METHODS[]
|
||||
) {
|
||||
@ -14,15 +14,15 @@ export default function isAllowedMethod(
|
||||
if (!req.method || !methods.includes(req.method)) {
|
||||
throw new CommerceAPIResponseError(
|
||||
`The HTTP ${req.method} method is not supported at this route.`,
|
||||
NextResponse.json(
|
||||
{
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
errors: [
|
||||
{
|
||||
code: 'invalid_method',
|
||||
message: `The HTTP ${req.method} method is not supported at this route.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 405,
|
||||
headers: {
|
||||
@ -36,7 +36,7 @@ export default function isAllowedMethod(
|
||||
if (req.method === 'OPTIONS') {
|
||||
throw new CommerceAPIResponseError(
|
||||
'This is a CORS preflight request.',
|
||||
new NextResponse(null, {
|
||||
new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
Allow: methods.join(', '),
|
||||
|
@ -51,7 +51,7 @@
|
||||
"@vercel/commerce": "workspace:*",
|
||||
"cookie": "^0.4.1",
|
||||
"js-cookie": "^3.0.1",
|
||||
"@tsndr/cloudflare-worker-jwt": "^2.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.debounce": "^4.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@ -66,6 +66,7 @@
|
||||
"@types/chec__commerce.js": "^2.8.4",
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/jsonwebtoken": "^8.5.7",
|
||||
"@types/lodash.debounce": "^4.0.6",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^18.0.14",
|
||||
|
@ -1,3 +1,3 @@
|
||||
export default function getCheckout(...args: any[]) {
|
||||
export default function getCheckout(..._args: any[]) {
|
||||
return Promise.resolve({ data: null })
|
||||
}
|
||||
|
@ -3,16 +3,15 @@ import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
||||
import type { CheckoutSchema } from '@vercel/commerce/types/checkout'
|
||||
import type { CommercejsAPI } from '../..'
|
||||
|
||||
import submitCheckout from './submit-checkout'
|
||||
import getCheckout from './get-checkout'
|
||||
import submitCheckout from './submit-checkout'
|
||||
|
||||
export type CheckoutAPI = GetAPISchema<CommercejsAPI, CheckoutSchema>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
export const handlers: CheckoutEndpoint['handlers'] = {
|
||||
submitCheckout,
|
||||
getCheckout,
|
||||
submitCheckout,
|
||||
}
|
||||
|
||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||
|
@ -1,7 +1,9 @@
|
||||
import type { LoginEndpoint } from '.'
|
||||
|
||||
import { serialize } from 'cookie'
|
||||
|
||||
import sdkFetcherFunction from '../../utils/sdk-fetch'
|
||||
import { getDeploymentUrl } from '../../../utils/get-deployment-url'
|
||||
import type { LoginEndpoint } from '.'
|
||||
|
||||
const login: LoginEndpoint['handlers']['login'] = async ({
|
||||
req,
|
||||
@ -10,27 +12,23 @@ const login: LoginEndpoint['handlers']['login'] = async ({
|
||||
const sdkFetcher: typeof sdkFetcherFunction = sdkFetch
|
||||
const redirectUrl = getDeploymentUrl()
|
||||
const { searchParams } = new URL(req.url)
|
||||
try {
|
||||
const loginToken = searchParams.get('token')
|
||||
const loginToken = searchParams.get('token')
|
||||
|
||||
if (!loginToken) {
|
||||
return { redirectTo: redirectUrl }
|
||||
}
|
||||
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
|
||||
|
||||
return {
|
||||
redirectTo: redirectUrl,
|
||||
headers: {
|
||||
'Set-Cookie': serialize(customerCookie, jwt, {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24,
|
||||
path: '/',
|
||||
}),
|
||||
},
|
||||
}
|
||||
} catch {
|
||||
if (!loginToken) {
|
||||
return { redirectTo: redirectUrl }
|
||||
}
|
||||
|
||||
const { jwt } = await sdkFetcher('customer', 'getToken', loginToken, false)
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'Set-Cookie': serialize(customerCookie, jwt, {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24,
|
||||
path: '/',
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default login
|
||||
|
@ -1,16 +1,14 @@
|
||||
import Cookies from 'js-cookie'
|
||||
import {
|
||||
decode,
|
||||
type JwtData as CoreJwtData,
|
||||
} from '@tsndr/cloudflare-worker-jwt'
|
||||
import { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCustomer, {
|
||||
UseCustomer,
|
||||
} from '@vercel/commerce/customer/use-customer'
|
||||
import { CUSTOMER_COOKIE, API_URL } from '../constants'
|
||||
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import type { CustomerHook } from '@vercel/commerce/types/customer'
|
||||
|
||||
type JwtData = CoreJwtData & {
|
||||
import Cookies from 'js-cookie'
|
||||
import { decode, type JwtPayload } from 'jsonwebtoken'
|
||||
import useCustomer, {
|
||||
type UseCustomer,
|
||||
} from '@vercel/commerce/customer/use-customer'
|
||||
import { CUSTOMER_COOKIE, API_URL } from '../constants'
|
||||
|
||||
type JwtData = JwtPayload & {
|
||||
cid: string
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ const login: LoginEndpoint['handlers']['login'] = async ({
|
||||
token.accessTokenExpiration ? { expires: cookieExpirationDate } : {}
|
||||
)
|
||||
|
||||
return { data: response, headers: { 'Set-Cookie': authCookie } }
|
||||
return { data: null, headers: { 'Set-Cookie': authCookie } }
|
||||
} catch (error) {
|
||||
// Check if the email and password didn't match an existing account
|
||||
if (
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { KiboCommerceConfig } from './../index'
|
||||
import { getCookieExpirationDate } from '../../lib/get-cookie-expiration-date'
|
||||
import { prepareSetCookie } from '../../lib/prepare-set-cookie'
|
||||
import { setCookies } from '../../lib/set-cookie'
|
||||
|
||||
import getAnonymousShopperToken from './get-anonymous-shopper-token'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
const parseCookie = (cookieValue?: any) => {
|
||||
return cookieValue
|
||||
|
@ -3,7 +3,6 @@ import type { Provider, SaleorConfig } from '..'
|
||||
import { throwUserErrors } from '../../utils'
|
||||
|
||||
import * as Mutation from '../../utils/mutations'
|
||||
import type { NextResponse } from 'next/server'
|
||||
|
||||
export default function loginOperation({
|
||||
commerce,
|
||||
@ -15,7 +14,7 @@ export default function loginOperation({
|
||||
}: {
|
||||
query?: string
|
||||
variables: any
|
||||
res: NextResponse
|
||||
res: Response
|
||||
config?: SaleorConfig
|
||||
}): Promise<any> {
|
||||
config = commerce.getConfig(config)
|
||||
|
@ -1,22 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default function isAllowedMethod(req: NextApiRequest, res: NextApiResponse, allowedMethods: string[]) {
|
||||
const methods = allowedMethods.includes('OPTIONS') ? allowedMethods : [...allowedMethods, 'OPTIONS']
|
||||
|
||||
if (!req.method || !methods.includes(req.method)) {
|
||||
res.status(405)
|
||||
res.setHeader('Allow', methods.join(', '))
|
||||
res.end()
|
||||
return false
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200)
|
||||
res.setHeader('Allow', methods.join(', '))
|
||||
res.setHeader('Content-Length', '0')
|
||||
res.end()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -7,7 +7,6 @@ import {
|
||||
throwUserErrors,
|
||||
} from '../../utils'
|
||||
import { CustomerAccessTokenCreateMutation } from '../../../schema'
|
||||
import type { NextResponse } from 'next/server'
|
||||
|
||||
export default function loginOperation({
|
||||
commerce,
|
||||
@ -19,7 +18,7 @@ export default function loginOperation({
|
||||
}: {
|
||||
query?: string
|
||||
variables: T['variables']
|
||||
res: NextResponse
|
||||
res: Response
|
||||
config?: ShopifyConfig
|
||||
}): Promise<T['data']> {
|
||||
config = commerce.getConfig(config)
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
|
||||
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
@ -27,7 +26,7 @@ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
</html>
|
||||
`
|
||||
|
||||
return new NextResponse(html, {
|
||||
return new Response(html, {
|
||||
headers: {
|
||||
'content-type': 'text/html',
|
||||
},
|
||||
|
@ -1 +0,0 @@
|
||||
export default function () {}
|
@ -1 +0,0 @@
|
||||
export default function () {}
|
@ -1 +0,0 @@
|
||||
export default function () {}
|
@ -1 +0,0 @@
|
||||
export default function () {}
|
@ -1 +0,0 @@
|
||||
export default function () {}
|
@ -1 +0,0 @@
|
||||
export default function () {}
|
@ -3,7 +3,7 @@ import type {
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { LoginOperation } from '@vercel/commerce/types/login'
|
||||
import type { NextResponse } from 'next/server'
|
||||
|
||||
import { Provider, SwellConfig } from '..'
|
||||
|
||||
export default function loginOperation({
|
||||
@ -12,14 +12,14 @@ export default function loginOperation({
|
||||
async function login<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<SwellConfig>
|
||||
res: NextResponse
|
||||
res: Response
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function login<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<SwellConfig>
|
||||
res: NextResponse
|
||||
res: Response
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
@ -30,7 +30,7 @@ export default function loginOperation({
|
||||
}: {
|
||||
query?: string
|
||||
variables: T['variables']
|
||||
res: NextResponse
|
||||
res: Response
|
||||
config?: Partial<SwellConfig>
|
||||
}): Promise<T['data']> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
|
@ -1,2 +0,0 @@
|
||||
import zeitFetch from '@vercel/fetch'
|
||||
export default zeitFetch()
|
@ -1,28 +0,0 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
export default function isAllowedMethod(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
allowedMethods: string[]
|
||||
) {
|
||||
const methods = allowedMethods.includes('OPTIONS')
|
||||
? allowedMethods
|
||||
: [...allowedMethods, 'OPTIONS']
|
||||
|
||||
if (!req.method || !methods.includes(req.method)) {
|
||||
res.status(405)
|
||||
res.setHeader('Allow', methods.join(', '))
|
||||
res.end()
|
||||
return false
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(200)
|
||||
res.setHeader('Allow', methods.join(', '))
|
||||
res.setHeader('Content-Length', '0')
|
||||
res.end()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
@ -7,7 +7,6 @@ import type { LoginOperation } from '@vercel/commerce/types/login'
|
||||
import type { LoginMutation } from '../../../schema'
|
||||
import { Provider, VendureConfig } from '..'
|
||||
import { loginMutation } from '../../utils/mutations/log-in-mutation'
|
||||
import type { NextResponse } from 'next/server'
|
||||
|
||||
export default function loginOperation({
|
||||
commerce,
|
||||
@ -15,14 +14,14 @@ export default function loginOperation({
|
||||
async function login<T extends LoginOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<VendureConfig>
|
||||
res: NextResponse
|
||||
res: Response
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function login<T extends LoginOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<VendureConfig>
|
||||
res: NextResponse
|
||||
res: Response
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
@ -34,7 +33,7 @@ export default function loginOperation({
|
||||
}: {
|
||||
query?: string
|
||||
variables: T['variables']
|
||||
res: NextResponse
|
||||
res: Response
|
||||
config?: Partial<VendureConfig>
|
||||
}): Promise<T['data']> {
|
||||
const config = commerce.getConfig(cfg)
|
||||
|
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -20,6 +20,7 @@ importers:
|
||||
'@taskr/watch': ^1.1.0
|
||||
'@tsndr/cloudflare-worker-jwt': ^2.1.0
|
||||
'@types/cookie': ^0.4.1
|
||||
'@types/jsonwebtoken': ^8.5.7
|
||||
'@types/lodash.debounce': ^4.0.6
|
||||
'@types/node': ^17.0.8
|
||||
'@types/node-fetch': ^2.6.2
|
||||
@ -28,6 +29,7 @@ importers:
|
||||
cookie: ^0.4.1
|
||||
immutability-helper: ^3.1.1
|
||||
js-cookie: ^3.0.1
|
||||
jsonwebtoken: ^8.5.1
|
||||
lint-staged: ^12.1.7
|
||||
lodash.debounce: ^4.0.8
|
||||
next: ^12.0.8
|
||||
@ -37,6 +39,7 @@ importers:
|
||||
taskr: ^1.1.0
|
||||
taskr-swc: ^0.0.1
|
||||
typescript: ^4.7.4
|
||||
uuidv4: ^6.2.13
|
||||
dependencies:
|
||||
'@cfworker/uuid': 1.12.4
|
||||
'@tsndr/cloudflare-worker-jwt': 2.1.0
|
||||
@ -44,12 +47,15 @@ importers:
|
||||
cookie: 0.4.2
|
||||
immutability-helper: 3.1.1
|
||||
js-cookie: 3.0.1
|
||||
jsonwebtoken: 8.5.1
|
||||
lodash.debounce: 4.0.8
|
||||
uuidv4: 6.2.13
|
||||
devDependencies:
|
||||
'@taskr/clear': 1.1.0
|
||||
'@taskr/esnext': 1.1.0
|
||||
'@taskr/watch': 1.1.0
|
||||
'@types/cookie': 0.4.1
|
||||
'@types/jsonwebtoken': 8.5.9
|
||||
'@types/lodash.debounce': 4.0.7
|
||||
'@types/node': 17.0.45
|
||||
'@types/node-fetch': 2.6.2
|
||||
@ -3193,6 +3199,10 @@ packages:
|
||||
/@types/scheduler/0.16.2:
|
||||
resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
|
||||
|
||||
/@types/uuid/8.3.4:
|
||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||
dev: false
|
||||
|
||||
/@types/ws/8.5.3:
|
||||
resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
|
||||
dependencies:
|
||||
@ -9473,6 +9483,18 @@ packages:
|
||||
lodash.isplainobject: 4.0.6
|
||||
dev: false
|
||||
|
||||
/uuid/8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/uuidv4/6.2.13:
|
||||
resolution: {integrity: sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ==}
|
||||
dependencies:
|
||||
'@types/uuid': 8.3.4
|
||||
uuid: 8.3.2
|
||||
dev: false
|
||||
|
||||
/v8-compile-cache-lib/3.0.1:
|
||||
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
|
||||
dev: true
|
||||
|
Loading…
x
Reference in New Issue
Block a user