mirror of
https://github.com/vercel/commerce.git
synced 2025-06-19 21:51:21 +00:00
Vercel only side of password change
This commit is contained in:
parent
f39c5e1e48
commit
c25e8eff9a
123
components/auth/ChangePassword.tsx
Normal file
123
components/auth/ChangePassword.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import { FC, useEffect, useState, useCallback } from 'react'
|
||||
import { Logo, Button, Input } from '@components/ui'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import useCustomer from '@framework/customer/use-customer'
|
||||
import useChangePassword from '@framework/auth/use-change-password'
|
||||
import changePassword from '@framework/api/endpoints/change-password/change-password'
|
||||
|
||||
// import { validate } from 'email-validator'
|
||||
|
||||
interface Props {
|
||||
}
|
||||
|
||||
const ChangePassword: FC<Props> = () => {
|
||||
const changePassword = useChangePassword()
|
||||
// return (<div>FOo</div>)
|
||||
//
|
||||
const { data: customer } = useCustomer()
|
||||
//
|
||||
if (customer) {
|
||||
console.log(customer)
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
||||
// Form State
|
||||
const [email, _setEmail] = useState(customer.email as string)
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [dirty, setDirty] = useState(false)
|
||||
const [disabled, setDisabled] = useState(false)
|
||||
const { setModalView, closeModal } = useUI()
|
||||
|
||||
// // const login = useLogin()
|
||||
//
|
||||
const handleChangePassword = async (e: React.SyntheticEvent<EventTarget>) => {
|
||||
console.log('handleChangePassword');
|
||||
e.preventDefault()
|
||||
|
||||
if (!dirty && !disabled) {
|
||||
setDirty(true)
|
||||
handleValidation()
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setMessage('')
|
||||
await changePassword({
|
||||
email,
|
||||
currentPassword,
|
||||
newPassword
|
||||
})
|
||||
setLoading(false)
|
||||
closeModal()
|
||||
} catch (error) {
|
||||
console.dir(error);
|
||||
const { errors } = error;
|
||||
console.dir(errors);
|
||||
setMessage(errors[0].message)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidation = useCallback(() => {
|
||||
// Test for Alphanumeric password
|
||||
const validPassword = /^(?=.*[a-zA-Z])(?=.*[0-9])/.test(newPassword)
|
||||
|
||||
// Unable to send form unless fields are valid.
|
||||
if (dirty) {
|
||||
setDisabled( newPassword.length < 7 || !validPassword ||newPassword != confirmPassword )
|
||||
}
|
||||
}, [newPassword, confirmPassword, dirty])
|
||||
|
||||
useEffect(() => {
|
||||
handleValidation()
|
||||
}, [handleValidation])
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleChangePassword}
|
||||
className='w-80 flex flex-col justify-between p-3'
|
||||
>
|
||||
<div className='flex justify-center pb-12 '>
|
||||
<Logo width='64px' height='64px' />
|
||||
</div>
|
||||
<div className='flex flex-col space-y-3'>
|
||||
{message && (
|
||||
<div className='text-red border border-red p-3'>
|
||||
{message}. Did you {` `}
|
||||
<a
|
||||
className='text-accent-9 inline font-bold hover:underline cursor-pointer'
|
||||
onClick={() => setModalView('FORGOT_VIEW')}
|
||||
>
|
||||
forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Input type='email' disabled={true} value={email} />
|
||||
<Input type='password' autoComplete={'current-password'} placeholder='Current Password' onChange={setCurrentPassword} />
|
||||
<Input type='password' autoComplete={'new-password'} placeholder='New Password' onChange={setNewPassword} />
|
||||
<Input type='password' autoComplete={'new-password'} placeholder='Confirm new Password' onChange={setConfirmPassword} />
|
||||
|
||||
<Button
|
||||
variant='slim'
|
||||
type='submit'
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
>
|
||||
Change Password
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export default ChangePassword
|
@ -1,3 +1,4 @@
|
||||
export { default as LoginView } from './LoginView'
|
||||
export { default as SignUpView } from './SignUpView'
|
||||
export { default as ForgotPassword } from './ForgotPassword'
|
||||
export { default as ChangePassword } from './ChangePassword'
|
||||
|
@ -42,6 +42,11 @@ const FeatureBar = dynamic(
|
||||
dynamicProps
|
||||
)
|
||||
|
||||
const ChangePassword = dynamic(
|
||||
() => import('@components/auth/ChangePassword'),
|
||||
dynamicProps
|
||||
)
|
||||
|
||||
interface Props {
|
||||
pageProps: {
|
||||
pages?: Page[]
|
||||
@ -58,6 +63,7 @@ const ModalView: FC<{ modalView: string; closeModal(): any }> = ({
|
||||
{modalView === 'LOGIN_VIEW' && <LoginView />}
|
||||
{modalView === 'SIGNUP_VIEW' && <SignUpView />}
|
||||
{modalView === 'FORGOT_VIEW' && <ForgotPassword />}
|
||||
{modalView === 'CHANGE_PASSWORD' && <ChangePassword />}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { BigcommerceApiError } from '../../utils/errors'
|
||||
import type { ChangePasswordEndpoint } from '.'
|
||||
|
||||
const changePassword: ChangePasswordEndpoint['handlers']['changePassword'] = async ({
|
||||
res,
|
||||
body: { email, currentPassword, newPassword },
|
||||
config,
|
||||
commerce
|
||||
}) => {
|
||||
// // TODO: Add proper validations with something like Ajv
|
||||
// if (!(email && password)) {
|
||||
// return res.status(400).json({
|
||||
// data: null,
|
||||
// errors: [{ message: 'Invalid request' }]
|
||||
// })
|
||||
// }
|
||||
// // TODO: validate the password and email
|
||||
// // Passwords must be at least 7 characters and contain both alphabetic
|
||||
// // and numeric characters.
|
||||
//
|
||||
// try {
|
||||
// await config.storeApiFetch('/v3/customers', {
|
||||
// method: 'POST',
|
||||
// body: JSON.stringify([
|
||||
// {
|
||||
// first_name: firstName,
|
||||
// last_name: lastName,
|
||||
// email,
|
||||
// authentication: {
|
||||
// new_password: password
|
||||
// }
|
||||
// }
|
||||
// ])
|
||||
// })
|
||||
// } catch (error) {
|
||||
// if (error instanceof BigcommerceApiError && error.status === 422) {
|
||||
// const hasEmailError = '0.email' in error.data?.errors
|
||||
//
|
||||
// // If there's an error with the email, it most likely means it's duplicated
|
||||
// if (hasEmailError) {
|
||||
// return res.status(400).json({
|
||||
// data: null,
|
||||
// errors: [
|
||||
// {
|
||||
// message: 'The email is already in use',
|
||||
// code: 'duplicated_email'
|
||||
// }
|
||||
// ]
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// throw error
|
||||
// }
|
||||
//
|
||||
// // Login the customer right after creating it
|
||||
// await commerce.login({ variables: { email, password }, res, config })
|
||||
|
||||
|
||||
const supplied = `YOU SUPPLIED: "${email}" "${currentPassword}" "${newPassword}"`
|
||||
console.log(supplied);
|
||||
res.status(200).json({ data: null, errors: [{message: supplied }] })
|
||||
}
|
||||
|
||||
export default changePassword
|
18
framework/bigcommerce/api/endpoints/change-password/index.ts
Normal file
18
framework/bigcommerce/api/endpoints/change-password/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||
import changePasswordEndpoint from '@commerce/api/endpoints/change-password'
|
||||
import type { ChangePasswordSchema } from '../../../types/change-password'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import changePassword from './change-password'
|
||||
|
||||
export type ChangePasswordAPI = GetAPISchema<BigcommerceAPI, ChangePasswordSchema>
|
||||
|
||||
export type ChangePasswordEndpoint = ChangePasswordAPI['endpoint']
|
||||
|
||||
export const handlers: ChangePasswordEndpoint['handlers'] = { changePassword }
|
||||
|
||||
const changePasswordApi = createEndpoint<ChangePasswordAPI>({
|
||||
handler: changePasswordEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default changePasswordApi
|
@ -1,3 +1,4 @@
|
||||
export { default as useLogin } from './use-login'
|
||||
export { default as useLogout } from './use-logout'
|
||||
export { default as useSignup } from './use-signup'
|
||||
export { default as useChangePassword } from './use-change-password'
|
||||
|
43
framework/bigcommerce/auth/use-change-password.ts
Normal file
43
framework/bigcommerce/auth/use-change-password.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@commerce/utils/types'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useChangePassword , { UseChangePassword } from '@commerce/auth/use-change-password'
|
||||
import type { ChangePasswordHook } from '../types/change-password'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
|
||||
export default useChangePassword as UseChangePassword<typeof handler>
|
||||
|
||||
export const handler: MutationHook<ChangePasswordHook> = {
|
||||
fetchOptions: {
|
||||
url: '/api/change-password',
|
||||
method: 'POST',
|
||||
},
|
||||
async fetcher({ input: { email, currentPassword, newPassword }, options, fetch }) {
|
||||
if (!(email && currentPassword && newPassword)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'An email, current password, and new password are required to change password',
|
||||
})
|
||||
}
|
||||
|
||||
console.log('fetcher')
|
||||
console.dir({ email, currentPassword, newPassword })
|
||||
|
||||
return fetch({
|
||||
...options,
|
||||
body: { email, currentPassword, newPassword },
|
||||
})
|
||||
},
|
||||
useHook: ({ fetch }) => () => {
|
||||
const { revalidate } = useCustomer()
|
||||
|
||||
return useCallback(
|
||||
async function changePassword(input) {
|
||||
const data = await fetch({ input })
|
||||
await revalidate()
|
||||
return data
|
||||
},
|
||||
[fetch, revalidate]
|
||||
)
|
||||
},
|
||||
}
|
@ -13,6 +13,7 @@ import { handler as useSearch } from './product/use-search'
|
||||
import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogout } from './auth/use-logout'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
import { handler as useChangePassword } from './auth/use-change-password'
|
||||
|
||||
import fetcher from './fetcher'
|
||||
|
||||
@ -28,7 +29,7 @@ export const bigcommerceProvider = {
|
||||
},
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
auth: { useLogin, useLogout, useSignup, useChangePassword },
|
||||
}
|
||||
|
||||
export type BigcommerceProvider = typeof bigcommerceProvider
|
||||
|
1
framework/bigcommerce/types/change-password.ts
Normal file
1
framework/bigcommerce/types/change-password.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from '@commerce/types/change-password'
|
35
framework/commerce/api/endpoints/change-password.ts
Normal file
35
framework/commerce/api/endpoints/change-password.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { ChangePasswordSchema } from '../../types/change-password'
|
||||
import { CommerceAPIError } from '../utils/errors'
|
||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
||||
import type { GetAPISchema } from '..'
|
||||
|
||||
const changePasswordEndpoint: GetAPISchema<
|
||||
any,
|
||||
ChangePasswordSchema<any>
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
POST: handlers['changePassword'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const body = req.body ?? {}
|
||||
return await handlers['changePassword']({ ...ctx, body })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
const message =
|
||||
error instanceof CommerceAPIError
|
||||
? 'An unexpected error ocurred with the Commerce API'
|
||||
: 'An unexpected error ocurred'
|
||||
|
||||
res.status(500).json({ data: null, errors: [{ message }] })
|
||||
}
|
||||
}
|
||||
|
||||
export default changePasswordEndpoint
|
@ -9,6 +9,7 @@ import type { SignupSchema } from '../types/signup'
|
||||
import type { ProductsSchema } from '../types/product'
|
||||
import type { WishlistSchema } from '../types/wishlist'
|
||||
import type { CheckoutSchema } from '../types/checkout'
|
||||
import type { ChangePasswordSchema } from '../types/change-password'
|
||||
import {
|
||||
defaultOperations,
|
||||
OPERATIONS,
|
||||
@ -25,6 +26,7 @@ export type APISchemas =
|
||||
| ProductsSchema
|
||||
| WishlistSchema
|
||||
| CheckoutSchema
|
||||
| ChangePasswordSchema
|
||||
|
||||
export type GetAPISchema<
|
||||
C extends CommerceAPI<any>,
|
||||
|
20
framework/commerce/auth/use-change-password.tsx
Normal file
20
framework/commerce/auth/use-change-password.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { useHook, useMutationHook } from '../utils/use-hook'
|
||||
import { mutationFetcher } from '../utils/default-fetcher'
|
||||
import type { MutationHook, HookFetcherFn } from '../utils/types'
|
||||
import type { Provider } from '..'
|
||||
import { ChangePasswordHook } from '@commerce/types/change-password'
|
||||
|
||||
export type UseChangePassword<
|
||||
H extends MutationHook<ChangePasswordHook<any>> = MutationHook<ChangePasswordHook>
|
||||
> = ReturnType<H['useHook']>
|
||||
|
||||
export const fetcher: HookFetcherFn<ChangePasswordHook> = mutationFetcher
|
||||
|
||||
const fn = (provider: Provider) => provider.auth?.useChangePassword!
|
||||
|
||||
const useChangePassword: UseChangePassword = (...args) => {
|
||||
const hook = useHook(fn)
|
||||
return useMutationHook({ fetcher, ...hook })(...args)
|
||||
}
|
||||
|
||||
export default useChangePassword
|
@ -15,6 +15,7 @@ import type {
|
||||
Signup,
|
||||
Login,
|
||||
Logout,
|
||||
ChangePassword
|
||||
} from '@commerce/types'
|
||||
|
||||
import type { Fetcher, SWRHook, MutationHook } from './utils/types'
|
||||
@ -44,6 +45,7 @@ export type Provider = CommerceConfig & {
|
||||
useSignup?: MutationHook<Signup.SignupHook>
|
||||
useLogin?: MutationHook<Login.LoginHook>
|
||||
useLogout?: MutationHook<Logout.LogoutHook>
|
||||
useChangePassword?: MutationHook<ChangePassword.ChangePasswordHook>
|
||||
}
|
||||
}
|
||||
|
||||
|
25
framework/commerce/types/change-password.ts
Normal file
25
framework/commerce/types/change-password.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export type ChangePasswordBody = {
|
||||
email: string
|
||||
currentPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
export type ChangePasswordTypes = {
|
||||
body: ChangePasswordBody
|
||||
}
|
||||
|
||||
export type ChangePasswordHook<T extends ChangePasswordTypes = ChangePasswordTypes> = {
|
||||
data: null
|
||||
body: T['body']
|
||||
actionInput: T['body']
|
||||
fetcherInput: T['body']
|
||||
}
|
||||
|
||||
export type ChangePasswordSchema<T extends ChangePasswordTypes = ChangePasswordTypes> = {
|
||||
endpoint: {
|
||||
options: {}
|
||||
handlers: {
|
||||
changePassword: ChangePasswordHook<T>
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import * as Cart from './cart'
|
||||
import * as ChangePassword from './change-password'
|
||||
import * as Checkout from './checkout'
|
||||
import * as Common from './common'
|
||||
import * as Customer from './customer'
|
||||
@ -12,6 +13,7 @@ import * as Wishlist from './wishlist'
|
||||
|
||||
export type {
|
||||
Cart,
|
||||
ChangePassword,
|
||||
Checkout,
|
||||
Common,
|
||||
Customer,
|
||||
|
4
pages/api/change-password.ts
Normal file
4
pages/api/change-password.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import changePasswordApi from '@framework/api/endpoints/change-password'
|
||||
import commerce from '@lib/api/commerce'
|
||||
|
||||
export default changePasswordApi(commerce)
|
@ -2,7 +2,9 @@ import type { GetStaticPropsContext } from 'next'
|
||||
import useCustomer from '@framework/customer/use-customer'
|
||||
import commerce from '@lib/api/commerce'
|
||||
import { Layout } from '@components/common'
|
||||
import { Container, Text } from '@components/ui'
|
||||
import { Container, Text, useUI } from '@components/ui'
|
||||
|
||||
|
||||
|
||||
export async function getStaticProps({
|
||||
preview,
|
||||
@ -21,7 +23,15 @@ export async function getStaticProps({
|
||||
}
|
||||
|
||||
export default function Profile() {
|
||||
const { openModal, setModalView } = useUI()
|
||||
|
||||
const { data } = useCustomer()
|
||||
|
||||
const triggerChangePassword = () => {
|
||||
setModalView('CHANGE_PASSWORD')
|
||||
openModal()
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Text variant="pageHeading">My Profile</Text>
|
||||
@ -38,6 +48,14 @@ export default function Profile() {
|
||||
<Text variant="sectionHeading">Email</Text>
|
||||
<span>{data.email}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<Text variant="sectionHeading">Change Password</Text>
|
||||
<a
|
||||
className="text-accent-9 inline font-bold hover:underline cursor-pointer"
|
||||
onClick={triggerChangePassword}
|
||||
>Change Password</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -23,8 +23,8 @@
|
||||
"@components/*": ["components/*"],
|
||||
"@commerce": ["framework/commerce"],
|
||||
"@commerce/*": ["framework/commerce/*"],
|
||||
"@framework": ["framework/local"],
|
||||
"@framework/*": ["framework/local/*"]
|
||||
"@framework": ["framework/bigcommerce"],
|
||||
"@framework/*": ["framework/bigcommerce/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
|
||||
@ -34,6 +34,11 @@
|
||||
"./framework/shopify",
|
||||
"./framework/swell",
|
||||
"./framework/vendure",
|
||||
"./framework/saleor"
|
||||
"./framework/saleor",
|
||||
"framework/saleor",
|
||||
"framework/shopify",
|
||||
"framework/swell",
|
||||
"framework/vendure",
|
||||
"framework/local"
|
||||
]
|
||||
}
|
||||
|
18
yarn.lock
18
yarn.lock
@ -1140,6 +1140,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
||||
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
||||
|
||||
"@types/uuid@8.3.1":
|
||||
version "8.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
|
||||
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
|
||||
|
||||
"@types/websocket@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a"
|
||||
@ -6044,6 +6049,19 @@ util@^0.12.0:
|
||||
safe-buffer "^5.1.2"
|
||||
which-typed-array "^1.1.2"
|
||||
|
||||
uuid@8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuidv4@^6.2.10:
|
||||
version "6.2.11"
|
||||
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.11.tgz#34d5a03324eb38296b87ae523a64233b5286cc27"
|
||||
integrity sha512-OTS4waH9KplrXNADKo+Q1kT9AHWr8DaC0S5F54RQzEwcUaEzBEWQQlJyDUw/u1bkRhJyqkqhLD4M4lbFbV+89g==
|
||||
dependencies:
|
||||
"@types/uuid" "8.3.1"
|
||||
uuid "8.3.2"
|
||||
|
||||
valid-url@1.0.9, valid-url@^1.0.9:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
|
||||
|
Loading…
x
Reference in New Issue
Block a user