Vercel only side of password change

This commit is contained in:
Garrow Bedrossian 2021-07-11 22:58:17 -04:00
parent f39c5e1e48
commit c25e8eff9a
19 changed files with 395 additions and 5 deletions

View 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

View File

@ -1,3 +1,4 @@
export { default as LoginView } from './LoginView' export { default as LoginView } from './LoginView'
export { default as SignUpView } from './SignUpView' export { default as SignUpView } from './SignUpView'
export { default as ForgotPassword } from './ForgotPassword' export { default as ForgotPassword } from './ForgotPassword'
export { default as ChangePassword } from './ChangePassword'

View File

@ -42,6 +42,11 @@ const FeatureBar = dynamic(
dynamicProps dynamicProps
) )
const ChangePassword = dynamic(
() => import('@components/auth/ChangePassword'),
dynamicProps
)
interface Props { interface Props {
pageProps: { pageProps: {
pages?: Page[] pages?: Page[]
@ -58,6 +63,7 @@ const ModalView: FC<{ modalView: string; closeModal(): any }> = ({
{modalView === 'LOGIN_VIEW' && <LoginView />} {modalView === 'LOGIN_VIEW' && <LoginView />}
{modalView === 'SIGNUP_VIEW' && <SignUpView />} {modalView === 'SIGNUP_VIEW' && <SignUpView />}
{modalView === 'FORGOT_VIEW' && <ForgotPassword />} {modalView === 'FORGOT_VIEW' && <ForgotPassword />}
{modalView === 'CHANGE_PASSWORD' && <ChangePassword />}
</Modal> </Modal>
) )
} }

View File

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

View 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

View File

@ -1,3 +1,4 @@
export { default as useLogin } from './use-login' export { default as useLogin } from './use-login'
export { default as useLogout } from './use-logout' export { default as useLogout } from './use-logout'
export { default as useSignup } from './use-signup' export { default as useSignup } from './use-signup'
export { default as useChangePassword } from './use-change-password'

View 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]
)
},
}

View File

@ -13,6 +13,7 @@ import { handler as useSearch } from './product/use-search'
import { handler as useLogin } from './auth/use-login' 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'
import { handler as useChangePassword } from './auth/use-change-password'
import fetcher from './fetcher' import fetcher from './fetcher'
@ -28,7 +29,7 @@ export const bigcommerceProvider = {
}, },
customer: { useCustomer }, customer: { useCustomer },
products: { useSearch }, products: { useSearch },
auth: { useLogin, useLogout, useSignup }, auth: { useLogin, useLogout, useSignup, useChangePassword },
} }
export type BigcommerceProvider = typeof bigcommerceProvider export type BigcommerceProvider = typeof bigcommerceProvider

View File

@ -0,0 +1 @@
export * from '@commerce/types/change-password'

View 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

View File

@ -9,6 +9,7 @@ import type { SignupSchema } from '../types/signup'
import type { ProductsSchema } from '../types/product' import type { ProductsSchema } from '../types/product'
import type { WishlistSchema } from '../types/wishlist' import type { WishlistSchema } from '../types/wishlist'
import type { CheckoutSchema } from '../types/checkout' import type { CheckoutSchema } from '../types/checkout'
import type { ChangePasswordSchema } from '../types/change-password'
import { import {
defaultOperations, defaultOperations,
OPERATIONS, OPERATIONS,
@ -25,6 +26,7 @@ export type APISchemas =
| ProductsSchema | ProductsSchema
| WishlistSchema | WishlistSchema
| CheckoutSchema | CheckoutSchema
| ChangePasswordSchema
export type GetAPISchema< export type GetAPISchema<
C extends CommerceAPI<any>, C extends CommerceAPI<any>,

View 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

View File

@ -15,6 +15,7 @@ import type {
Signup, Signup,
Login, Login,
Logout, Logout,
ChangePassword
} from '@commerce/types' } from '@commerce/types'
import type { Fetcher, SWRHook, MutationHook } from './utils/types' import type { Fetcher, SWRHook, MutationHook } from './utils/types'
@ -44,6 +45,7 @@ export type Provider = CommerceConfig & {
useSignup?: MutationHook<Signup.SignupHook> useSignup?: MutationHook<Signup.SignupHook>
useLogin?: MutationHook<Login.LoginHook> useLogin?: MutationHook<Login.LoginHook>
useLogout?: MutationHook<Logout.LogoutHook> useLogout?: MutationHook<Logout.LogoutHook>
useChangePassword?: MutationHook<ChangePassword.ChangePasswordHook>
} }
} }

View 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>
}
}
}

View File

@ -1,4 +1,5 @@
import * as Cart from './cart' import * as Cart from './cart'
import * as ChangePassword from './change-password'
import * as Checkout from './checkout' import * as Checkout from './checkout'
import * as Common from './common' import * as Common from './common'
import * as Customer from './customer' import * as Customer from './customer'
@ -12,6 +13,7 @@ import * as Wishlist from './wishlist'
export type { export type {
Cart, Cart,
ChangePassword,
Checkout, Checkout,
Common, Common,
Customer, Customer,

View File

@ -0,0 +1,4 @@
import changePasswordApi from '@framework/api/endpoints/change-password'
import commerce from '@lib/api/commerce'
export default changePasswordApi(commerce)

View File

@ -2,7 +2,9 @@ import type { GetStaticPropsContext } from 'next'
import useCustomer from '@framework/customer/use-customer' import useCustomer from '@framework/customer/use-customer'
import commerce from '@lib/api/commerce' import commerce from '@lib/api/commerce'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { Container, Text } from '@components/ui' import { Container, Text, useUI } from '@components/ui'
export async function getStaticProps({ export async function getStaticProps({
preview, preview,
@ -21,7 +23,15 @@ export async function getStaticProps({
} }
export default function Profile() { export default function Profile() {
const { openModal, setModalView } = useUI()
const { data } = useCustomer() const { data } = useCustomer()
const triggerChangePassword = () => {
setModalView('CHANGE_PASSWORD')
openModal()
};
return ( return (
<Container> <Container>
<Text variant="pageHeading">My Profile</Text> <Text variant="pageHeading">My Profile</Text>
@ -38,6 +48,14 @@ export default function Profile() {
<Text variant="sectionHeading">Email</Text> <Text variant="sectionHeading">Email</Text>
<span>{data.email}</span> <span>{data.email}</span>
</div> </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>
</div> </div>
)} )}

View File

@ -23,8 +23,8 @@
"@components/*": ["components/*"], "@components/*": ["components/*"],
"@commerce": ["framework/commerce"], "@commerce": ["framework/commerce"],
"@commerce/*": ["framework/commerce/*"], "@commerce/*": ["framework/commerce/*"],
"@framework": ["framework/local"], "@framework": ["framework/bigcommerce"],
"@framework/*": ["framework/local/*"] "@framework/*": ["framework/bigcommerce/*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], "include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
@ -34,6 +34,11 @@
"./framework/shopify", "./framework/shopify",
"./framework/swell", "./framework/swell",
"./framework/vendure", "./framework/vendure",
"./framework/saleor" "./framework/saleor",
"framework/saleor",
"framework/shopify",
"framework/swell",
"framework/vendure",
"framework/local"
] ]
} }

View File

@ -1140,6 +1140,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== 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": "@types/websocket@1.0.2":
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a" 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" safe-buffer "^5.1.2"
which-typed-array "^1.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: valid-url@1.0.9, valid-url@^1.0.9:
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"