mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 07:26:59 +00:00
feat(auth): draft auth implementation
This commit is contained in:
parent
4adba68c4c
commit
4b27c4849b
@ -1 +1,18 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||||
|
import loginEndpoint from '@commerce/api/endpoints/login'
|
||||||
|
import type { LoginSchema } from '../../../types/login'
|
||||||
|
import type { CommercelayerAPI } from '../..'
|
||||||
|
import login from './login'
|
||||||
|
|
||||||
|
export type LoginAPI = GetAPISchema<CommercelayerAPI, LoginSchema>
|
||||||
|
|
||||||
|
export type LoginEndpoint = LoginAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: LoginEndpoint['handlers'] = { login }
|
||||||
|
|
||||||
|
const loginApi = createEndpoint<LoginAPI>({
|
||||||
|
handler: loginEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default loginApi
|
47
framework/commercelayer/api/endpoints/login/login.ts
Normal file
47
framework/commercelayer/api/endpoints/login/login.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { FetcherError } from '@commerce/utils/errors'
|
||||||
|
import { getAccessToken } from '../../index'
|
||||||
|
import type { LoginEndpoint } from '.'
|
||||||
|
|
||||||
|
const login: LoginEndpoint['handlers']['login'] = async ({
|
||||||
|
res,
|
||||||
|
body: { email, password },
|
||||||
|
config,
|
||||||
|
commerce,
|
||||||
|
}) => {
|
||||||
|
if (!(email && password)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = { email, password };
|
||||||
|
const getToken = await getAccessToken(user);
|
||||||
|
await config.apiFetch(`/api/customers/${getToken.customerId}`, {
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof FetcherError &&
|
||||||
|
/invalid credentials/i.test(error.message)) {
|
||||||
|
return res.status(401).json({
|
||||||
|
data: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message:
|
||||||
|
'Cannot find an account that matches the provided credentials',
|
||||||
|
code: 'INVALID_CREDENTIALS',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
await commerce.login({ variables: { email, password }, config, res })
|
||||||
|
|
||||||
|
res.status(200).json({ data: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default login
|
@ -1 +1,18 @@
|
|||||||
export default function noopApi(...args: any[]): void {}
|
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||||
|
import signupEndpoint from '@commerce/api/endpoints/signup'
|
||||||
|
import type { SignupSchema } from '../../../types/signup'
|
||||||
|
import type { CommercelayerAPI } from '../..'
|
||||||
|
import signup from './signup'
|
||||||
|
|
||||||
|
export type SignupAPI = GetAPISchema<CommercelayerAPI, SignupSchema>
|
||||||
|
|
||||||
|
export type SignupEndpoint = SignupAPI['endpoint']
|
||||||
|
|
||||||
|
export const handlers: SignupEndpoint['handlers'] = { signup }
|
||||||
|
|
||||||
|
const singupApi = createEndpoint<SignupAPI>({
|
||||||
|
handler: signupEndpoint,
|
||||||
|
handlers,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default singupApi
|
||||||
|
55
framework/commercelayer/api/endpoints/signup/signup.ts
Normal file
55
framework/commercelayer/api/endpoints/signup/signup.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { CommercelayerApiError } from '../../utils/errors'
|
||||||
|
import type { SignupEndpoint } from '.'
|
||||||
|
|
||||||
|
const signup: SignupEndpoint['handlers']['signup'] = async ({
|
||||||
|
res,
|
||||||
|
body: { email, password },
|
||||||
|
config,
|
||||||
|
commerce,
|
||||||
|
}) => {
|
||||||
|
if (!(email && password)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [{ message: 'Invalid request' }],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await config.apiFetch('/api/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
data: {
|
||||||
|
type: 'customers',
|
||||||
|
attributes: {
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CommercelayerApiError && error.status === 422) {
|
||||||
|
const inputEmail = error.data?.errors.meta.value
|
||||||
|
|
||||||
|
if ('code' in error.data?.errors) {
|
||||||
|
return res.status(400).json({
|
||||||
|
data: null,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: `A user already exists with ${inputEmail}`,
|
||||||
|
code: 'USER_EXISTS',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
await commerce.login({ variables: { email, password }, res, config })
|
||||||
|
|
||||||
|
res.status(200).json({ data: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default signup
|
@ -12,7 +12,6 @@ import getAllProducts from './operations/get-all-products'
|
|||||||
import getProduct from './operations/get-product'
|
import getProduct from './operations/get-product'
|
||||||
|
|
||||||
import { getToken } from './utils/get-token'
|
import { getToken } from './utils/get-token'
|
||||||
import fetch from './utils/fetch'
|
|
||||||
|
|
||||||
export interface CommercelayerConfig extends Omit<CommerceAPIConfig, 'fetch'> {
|
export interface CommercelayerConfig extends Omit<CommerceAPIConfig, 'fetch'> {
|
||||||
apiClientId: string
|
apiClientId: string
|
||||||
@ -36,19 +35,19 @@ const MARKET_SCOPE = process.env.COMMERCELAYER_MARKET_SCOPE
|
|||||||
|
|
||||||
if (!CLIENT_ID) {
|
if (!CLIENT_ID) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The environment variable COMMERCELAYER_CLIENT_ID is missing and it's required to access your store`
|
`The environment variable COMMERCELAYER_CLIENT_ID is missing and it's required.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ENDPOINT) {
|
if (!ENDPOINT) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The environment variable COMMERCELAYER_ENDPOINT is missing and it's required to access your store`
|
`The environment variable COMMERCELAYER_ENDPOINT is missing and it's required.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MARKET_SCOPE) {
|
if (!MARKET_SCOPE) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`The environment variable COMMERCELAYER_MARKET_SCOPE is missing and it's required to access your store`
|
`The environment variable COMMERCELAYER_MARKET_SCOPE is missing and it's required.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
framework/commercelayer/api/utils/errors.ts
Normal file
22
framework/commercelayer/api/utils/errors.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { Response } from '@vercel/fetch'
|
||||||
|
|
||||||
|
export class CommercelayerApiError extends Error {
|
||||||
|
status: number
|
||||||
|
res: Response
|
||||||
|
data: any
|
||||||
|
|
||||||
|
constructor(msg: string, res: Response, data?: any) {
|
||||||
|
super(msg)
|
||||||
|
this.name = 'CommercelayerApiError'
|
||||||
|
this.status = res.status
|
||||||
|
this.res = res
|
||||||
|
this.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommercelayerNetworkError extends Error {
|
||||||
|
constructor(msg: string) {
|
||||||
|
super(msg)
|
||||||
|
this.name = 'CommercelayerNetworkError'
|
||||||
|
}
|
||||||
|
}
|
@ -16,21 +16,20 @@ const fetchApi =
|
|||||||
const token = getToken?.accessToken
|
const token = getToken?.accessToken
|
||||||
const res = await fetch(config.commerceUrl + endpoint, {
|
const res = await fetch(config.commerceUrl + endpoint, {
|
||||||
...fetchOptions,
|
...fetchOptions,
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
headers: {
|
||||||
...fetchOptions?.headers,
|
...fetchOptions?.headers,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify([
|
||||||
query,
|
query,
|
||||||
}),
|
]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const json = await res.json()
|
const json = await res.json()
|
||||||
if (json.errors) {
|
if (json.errors) {
|
||||||
throw new FetcherError({
|
throw new FetcherError({
|
||||||
errors: json.errors ?? [{ message: 'Failed to fetch for API' }],
|
errors: json.errors ?? [{ message: 'Failed to fetch Commerce Layer API' }],
|
||||||
status: res.status,
|
status: res.status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,40 @@
|
|||||||
import { MutationHook } from '@commerce/utils/types'
|
import { useCallback } from 'react'
|
||||||
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
import useLogin, { UseLogin } from '@commerce/auth/use-login'
|
||||||
|
import type { LoginHook } from '../types/login'
|
||||||
|
import useCustomer from '../customer/use-customer'
|
||||||
|
|
||||||
export default useLogin as UseLogin<typeof handler>
|
export default useLogin as UseLogin<typeof handler>
|
||||||
|
|
||||||
export const handler: MutationHook<any> = {
|
export const handler: MutationHook<LoginHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
query: '',
|
url: '/api/customers',
|
||||||
|
method: 'GET',
|
||||||
},
|
},
|
||||||
async fetcher() {
|
async fetcher({ input: { email, password }, options, fetch }) {
|
||||||
return null
|
if (!(email && password)) {
|
||||||
|
throw new CommerceError({
|
||||||
|
message:
|
||||||
|
'An email address and password are required to login',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch({
|
||||||
|
...options,
|
||||||
|
body: { email, password },
|
||||||
|
})
|
||||||
},
|
},
|
||||||
useHook: () => () => {
|
useHook: ({ fetch }) => () => {
|
||||||
return async function () {}
|
const { revalidate } = useCustomer()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function login(input) {
|
||||||
|
const data = await fetch({ input })
|
||||||
|
await revalidate()
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
[fetch, revalidate]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -1,19 +1,44 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import useCustomer from '../customer/use-customer'
|
import type { MutationHook } from '@commerce/utils/types'
|
||||||
import { MutationHook } from '@commerce/utils/types'
|
import { CommerceError } from '@commerce/utils/errors'
|
||||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
import useSignup, { UseSignup } from '@commerce/auth/use-signup'
|
||||||
|
import type { SignupHook } from '../types/signup'
|
||||||
|
import useCustomer from '../customer/use-customer'
|
||||||
|
|
||||||
export default useSignup as UseSignup<typeof handler>
|
export default useSignup as UseSignup<typeof handler>
|
||||||
|
|
||||||
export const handler: MutationHook<any> = {
|
export const handler: MutationHook<SignupHook> = {
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
query: '',
|
url: '/api/customers',
|
||||||
|
method: 'POST',
|
||||||
|
},
|
||||||
|
async fetcher({
|
||||||
|
input: { email, password },
|
||||||
|
options,
|
||||||
|
fetch,
|
||||||
|
}) {
|
||||||
|
if (!(email && password)) {
|
||||||
|
throw new CommerceError({
|
||||||
|
message:
|
||||||
|
'An email address and password are required to signup',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch({
|
||||||
|
...options,
|
||||||
|
body: { email, password },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
useHook: ({ fetch }) => () => {
|
||||||
|
const { revalidate } = useCustomer()
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function signup(input) {
|
||||||
|
const data = await fetch({ input })
|
||||||
|
await revalidate()
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
[fetch, revalidate]
|
||||||
|
)
|
||||||
},
|
},
|
||||||
async fetcher() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
useHook:
|
|
||||||
({ fetch }) =>
|
|
||||||
() =>
|
|
||||||
() => {},
|
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"provider": "local",
|
"provider": "commercelayer",
|
||||||
"features": {
|
"features": {
|
||||||
"wishlist": false,
|
"wishlist": false,
|
||||||
"cart": false,
|
"cart": false,
|
||||||
"search": false,
|
"search": false,
|
||||||
"customerAuth": false
|
"customerAuth": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { localProvider } from './provider'
|
import { commerceLayerProvider } from './provider'
|
||||||
import {
|
import {
|
||||||
CommerceConfig,
|
CommerceConfig,
|
||||||
CommerceProvider as CoreCommerceProvider,
|
CommerceProvider as CoreCommerceProvider,
|
||||||
useCommerce as useCoreCommerce,
|
useCommerce as useCoreCommerce,
|
||||||
} from '@commerce'
|
} from '@commerce'
|
||||||
|
|
||||||
export const localConfig: CommerceConfig = {
|
export const commerceLayerConfig: CommerceConfig = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'session',
|
cartCookie: 'session',
|
||||||
}
|
}
|
||||||
@ -21,8 +21,8 @@ export function CommerceProvider({
|
|||||||
} & Partial<CommerceConfig>) {
|
} & Partial<CommerceConfig>) {
|
||||||
return (
|
return (
|
||||||
<CoreCommerceProvider
|
<CoreCommerceProvider
|
||||||
provider={localProvider}
|
provider={commerceLayerProvider}
|
||||||
config={{ ...localConfig, ...config }}
|
config={{ ...commerceLayerConfig, ...config }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CoreCommerceProvider>
|
</CoreCommerceProvider>
|
||||||
|
@ -9,8 +9,8 @@ import { handler as useLogin } from './auth/use-login'
|
|||||||
import { handler as useLogout } from './auth/use-logout'
|
import { handler as useLogout } from './auth/use-logout'
|
||||||
import { handler as useSignup } from './auth/use-signup'
|
import { handler as useSignup } from './auth/use-signup'
|
||||||
|
|
||||||
export type Provider = typeof localProvider
|
export type Provider = typeof commerceLayerProvider
|
||||||
export const localProvider = {
|
export const commerceLayerProvider = {
|
||||||
locale: 'en-us',
|
locale: 'en-us',
|
||||||
cartCookie: 'session',
|
cartCookie: 'session',
|
||||||
fetcher: fetcher,
|
fetcher: fetcher,
|
||||||
|
1
framework/commercelayer/types/cart.ts
Normal file
1
framework/commercelayer/types/cart.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/cart'
|
1
framework/commercelayer/types/checkout.ts
Normal file
1
framework/commercelayer/types/checkout.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/checkout'
|
1
framework/commercelayer/types/common.ts
Normal file
1
framework/commercelayer/types/common.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/common'
|
1
framework/commercelayer/types/customer.ts
Normal file
1
framework/commercelayer/types/customer.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/customer'
|
25
framework/commercelayer/types/index.ts
Normal file
25
framework/commercelayer/types/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as Cart from './cart'
|
||||||
|
import * as Checkout from './checkout'
|
||||||
|
import * as Common from './common'
|
||||||
|
import * as Customer from './customer'
|
||||||
|
import * as Login from './login'
|
||||||
|
import * as Logout from './logout'
|
||||||
|
import * as Page from './page'
|
||||||
|
import * as Product from './product'
|
||||||
|
import * as Signup from './signup'
|
||||||
|
import * as Site from './site'
|
||||||
|
import * as Wishlist from './wishlist'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Cart,
|
||||||
|
Checkout,
|
||||||
|
Common,
|
||||||
|
Customer,
|
||||||
|
Login,
|
||||||
|
Logout,
|
||||||
|
Page,
|
||||||
|
Product,
|
||||||
|
Signup,
|
||||||
|
Site,
|
||||||
|
Wishlist,
|
||||||
|
}
|
11
framework/commercelayer/types/login.ts
Normal file
11
framework/commercelayer/types/login.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as Core from '@commerce/types/login'
|
||||||
|
import { LoginBody, LoginTypes } from '@commerce/types/login'
|
||||||
|
|
||||||
|
export * from '@commerce/types/login'
|
||||||
|
|
||||||
|
export type LoginHook<T extends LoginTypes = LoginTypes> = {
|
||||||
|
data: null
|
||||||
|
actionInput: LoginBody
|
||||||
|
fetcherInput: LoginBody
|
||||||
|
body: T['body']
|
||||||
|
}
|
1
framework/commercelayer/types/logout.ts
Normal file
1
framework/commercelayer/types/logout.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/logout'
|
1
framework/commercelayer/types/page.ts
Normal file
1
framework/commercelayer/types/page.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/page'
|
1
framework/commercelayer/types/product.ts
Normal file
1
framework/commercelayer/types/product.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/product'
|
1
framework/commercelayer/types/signup.ts
Normal file
1
framework/commercelayer/types/signup.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/signup'
|
1
framework/commercelayer/types/site.ts
Normal file
1
framework/commercelayer/types/site.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/site'
|
1
framework/commercelayer/types/wishlist.ts
Normal file
1
framework/commercelayer/types/wishlist.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from '@commerce/types/wishlist'
|
@ -24,8 +24,8 @@
|
|||||||
"@components/*": ["components/*"],
|
"@components/*": ["components/*"],
|
||||||
"@commerce": ["framework/commerce"],
|
"@commerce": ["framework/commerce"],
|
||||||
"@commerce/*": ["framework/commerce/*"],
|
"@commerce/*": ["framework/commerce/*"],
|
||||||
"@framework": ["framework/local"],
|
"@framework": ["framework/commercelayer"],
|
||||||
"@framework/*": ["framework/local/*"]
|
"@framework/*": ["framework/commercelayer/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user