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 { getToken } from './utils/get-token'
|
||||
import fetch from './utils/fetch'
|
||||
|
||||
export interface CommercelayerConfig extends Omit<CommerceAPIConfig, 'fetch'> {
|
||||
apiClientId: string
|
||||
@ -36,19 +35,19 @@ const MARKET_SCOPE = process.env.COMMERCELAYER_MARKET_SCOPE
|
||||
|
||||
if (!CLIENT_ID) {
|
||||
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) {
|
||||
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) {
|
||||
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 res = await fetch(config.commerceUrl + endpoint, {
|
||||
...fetchOptions,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...fetchOptions?.headers,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
body: JSON.stringify([
|
||||
query,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
const json = await res.json()
|
||||
if (json.errors) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
@ -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 type { LoginHook } from '../types/login'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
export const handler: MutationHook<LoginHook> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
url: '/api/customers',
|
||||
method: 'GET',
|
||||
},
|
||||
async fetcher() {
|
||||
return null
|
||||
async fetcher({ input: { email, password }, options, fetch }) {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'An email address and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, password },
|
||||
})
|
||||
},
|
||||
useHook: () => () => {
|
||||
return async function () {}
|
||||
useHook: ({ fetch }) => () => {
|
||||
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 useCustomer from '../customer/use-customer'
|
||||
import { MutationHook } from '@commerce/utils/types'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
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 const handler: MutationHook<any> = {
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
fetchOptions: {
|
||||
query: '',
|
||||
url: '/api/customers',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher() {
|
||||
return null
|
||||
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]
|
||||
)
|
||||
},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
() => {},
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
{
|
||||
"provider": "local",
|
||||
"provider": "commercelayer",
|
||||
"features": {
|
||||
"wishlist": false,
|
||||
"cart": false,
|
||||
"search": false,
|
||||
"customerAuth": false
|
||||
"customerAuth": true
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import * as React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { localProvider } from './provider'
|
||||
import { commerceLayerProvider } from './provider'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
|
||||
export const localConfig: CommerceConfig = {
|
||||
export const commerceLayerConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'session',
|
||||
}
|
||||
@ -21,8 +21,8 @@ export function CommerceProvider({
|
||||
} & Partial<CommerceConfig>) {
|
||||
return (
|
||||
<CoreCommerceProvider
|
||||
provider={localProvider}
|
||||
config={{ ...localConfig, ...config }}
|
||||
provider={commerceLayerProvider}
|
||||
config={{ ...commerceLayerConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
|
@ -9,8 +9,8 @@ import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogout } from './auth/use-logout'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
|
||||
export type Provider = typeof localProvider
|
||||
export const localProvider = {
|
||||
export type Provider = typeof commerceLayerProvider
|
||||
export const commerceLayerProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'session',
|
||||
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/*"],
|
||||
"@commerce": ["framework/commerce"],
|
||||
"@commerce/*": ["framework/commerce/*"],
|
||||
"@framework": ["framework/local"],
|
||||
"@framework/*": ["framework/local/*"]
|
||||
"@framework": ["framework/commercelayer"],
|
||||
"@framework/*": ["framework/commercelayer/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
||||
|
Loading…
x
Reference in New Issue
Block a user