Icky-169-LogIn (#4)

* Update README.md

* Initial Commit

* Commited Keys

* GraphQL Changes

* GraphQL Query

* Final Changes

* Changed login.ts

* Made changes in login.ts

* Final Changes

* Refactored login.ts

* SignUp Initial Checkin

* logout Initial

* Customer Account Initial Commit

* Logout - deleted cookie

* Reverted ReadMe and UserNav file

* Final Changes

* Resolved comments

* Resolved comments 1

* Resolved comments 2

* Resolved comments 3

* Resolved comments 4

Co-authored-by: SushantJadhav <Sushant.Jadhav@kibocommerce.com>
This commit is contained in:
kibo-sushant 2021-08-31 05:05:31 +05:30 committed by GitHub
parent 449d7fee72
commit 327cc2f055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 511 additions and 38 deletions

View File

@ -0,0 +1,39 @@
import type { CustomerEndpoint } from '.'
import { getCustomerAccountQuery } from '../../queries/get-customer-account-query'
const getLoggedInCustomer: CustomerEndpoint['handlers']['getLoggedInCustomer'] = async ({
req,
res,
config,
}) => {
const token = req.cookies[config.customerCookie]
const { accessToken } = JSON.parse(token);
if (accessToken) {
const { data } = await config.fetch(
getCustomerAccountQuery,
undefined,
{
headers: {
'x-vol-user-claims': accessToken
},
}
)
const customer = data?.customerAccount;
if (!customer) {
return res.status(400).json({
data: null,
errors: [{ message: 'Customer not found', code: 'not_found' }],
})
}
return res.status(200).json({ data: { customer } })
}
res.status(200).json({ data: null })
}
export default getLoggedInCustomer

View File

@ -1 +1,18 @@
export default function noopApi(...args: any[]): void {} import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerEndpoint from '@commerce/api/endpoints/customer'
import type { CustomerSchema } from '../../../types/customer'
import type { KiboCommerceAPI } from '../..'
import getLoggedInCustomer from './customer'
export type CustomerAPI = GetAPISchema<KiboCommerceAPI, CustomerSchema>
export type CustomerEndpoint = CustomerAPI['endpoint']
export const handlers: CustomerEndpoint['handlers'] = { getLoggedInCustomer }
const customerApi = createEndpoint<CustomerAPI>({
handler: customerEndpoint,
handlers,
})
export default customerApi

View File

@ -1 +1,22 @@
export default function noopApi(...args: any[]): void {} // 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 { KiboCommerceAPI } from '../..'
import login from './login'
export type LoginAPI = GetAPISchema<KiboCommerceAPI, LoginSchema>
export type LoginEndpoint = LoginAPI['endpoint']
export const handlers: LoginEndpoint['handlers'] = { login }
const loginApi = createEndpoint<LoginAPI>({
handler: loginEndpoint,
handlers,
})
export default loginApi;

View File

@ -0,0 +1,66 @@
import { FetcherError } from '@commerce/utils/errors'
import type { LoginEndpoint } from '.'
import { loginMutation } from '../../mutations/login-mutation'
import { prepareSetCookie } from '../../../lib/prepareSetCookie';
import { setCookies } from '../../../lib/setCookie'
import { getCookieExpirationDate } from '../../../lib/getCookieExpirationDate'
const invalidCredentials = /invalid credentials/i
const login: LoginEndpoint['handlers']['login'] = async ({
req,
res,
body: { email, password },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
try {
const variables = { loginInput : { username: email, password }};
response = await config.fetch(loginMutation, { variables })
const { account: token } = response.data;
// Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays)
const authCookie = prepareSetCookie(
config.customerCookie,
JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {},
)
setCookies(res, [authCookie])
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.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
}
res.status(200).json({ data: response })
}
export default login

View File

@ -1 +1,18 @@
export default function noopApi(...args: any[]): void {} import { GetAPISchema, createEndpoint } from '@commerce/api'
import logoutEndpoint from '@commerce/api/endpoints/logout'
import type { LogoutSchema } from '../../../types/logout'
import type { KiboCommerceAPI } from '../..'
import logout from './logout'
export type LogoutAPI = GetAPISchema<KiboCommerceAPI, LogoutSchema>
export type LogoutEndpoint = LogoutAPI['endpoint']
export const handlers: LogoutEndpoint['handlers'] = { logout }
const logoutApi = createEndpoint<LogoutAPI>({
handler: logoutEndpoint,
handlers,
})
export default logoutApi

View File

@ -0,0 +1,22 @@
import type { LogoutEndpoint } from '.'
import {prepareSetCookie} from '../../../lib/prepareSetCookie';
import {setCookies} from '../../../lib/setCookie'
const logout: LogoutEndpoint['handlers']['logout'] = async ({
res,
body: { redirectTo },
config,
}) => {
// Remove the cookie
const authCookie = prepareSetCookie(config.customerCookie,'',{ maxAge: -1, path: '/' })
setCookies(res, [authCookie])
// Only allow redirects to a relative URL
if (redirectTo?.startsWith('/')) {
res.redirect(redirectTo)
} else {
res.status(200).json({ data: null })
}
}
export default logout

View File

@ -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 { KiboCommerceAPI } from '../..'
import signup from './signup'
export type SignupAPI = GetAPISchema<KiboCommerceAPI, SignupSchema>
export type SignupEndpoint = SignupAPI['endpoint']
export const handlers: SignupEndpoint['handlers'] = { signup }
const singupApi = createEndpoint<SignupAPI>({
handler: signupEndpoint,
handlers,
})
export default singupApi

View File

@ -0,0 +1,91 @@
import { FetcherError } from '@commerce/utils/errors'
import type { SignupEndpoint } from '.'
import { registerUserMutation, registerUserLoginMutation } from '../../mutations/signup-mutation'
import { prepareSetCookie } from '../../../lib/prepareSetCookie';
import { setCookies } from '../../../lib/setCookie'
import { getCookieExpirationDate } from '../../../lib/getCookieExpirationDate'
const invalidCredentials = /invalid credentials/i
const signup: SignupEndpoint['handlers']['signup'] = async ({
req,
res,
body: { email, password, firstName, lastName },
config,
commerce,
}) => {
if (!(email && password)) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
let response;
try {
// Register user
const registerUserVariables = {
customerAccountInput: {
emailAddress: email,
firstName: firstName,
lastName: lastName,
acceptsMarketing: true,
id: 0
}
}
const registerUserResponse = await config.fetch(registerUserMutation, { variables: registerUserVariables})
const accountId = registerUserResponse.data?.account?.id;
// Login user
const registerUserLoginVairables = {
accountId: accountId,
customerLoginInfoInput: {
emailAddress: email,
username: email,
password: password,
isImport: false
}
}
response = await config.fetch(registerUserLoginMutation, { variables: registerUserLoginVairables})
const { account: token } = response.data;
// Set Cookie
const cookieExpirationDate = getCookieExpirationDate(config.customerCookieMaxAgeInDays)
const authCookie = prepareSetCookie(
config.customerCookie,
JSON.stringify(token),
token.accessTokenExpiration ? { expires: cookieExpirationDate }: {},
)
setCookies(res, [authCookie])
} catch (error) {
// Check if the email and password didn't match an existing account
if (
error instanceof FetcherError &&
invalidCredentials.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
}
res.status(200).json({ data: response })
}
export default signup

View File

@ -17,6 +17,7 @@ export interface KiboCommerceConfig extends CommerceAPIConfig {
clientId?: string clientId?: string
sharedSecret?: string sharedSecret?: string
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T> storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
customerCookieMaxAgeInDays: number
} }
const config: KiboCommerceConfig = { const config: KiboCommerceConfig = {
@ -31,6 +32,7 @@ const config: KiboCommerceConfig = {
clientId: process.env.KIBO_CLIENT_ID || '', clientId: process.env.KIBO_CLIENT_ID || '',
sharedSecret: process.env.KIBO_SHARED_SECRET || '', sharedSecret: process.env.KIBO_SHARED_SECRET || '',
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()), storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
customerCookieMaxAgeInDays: 30,
} }
const operations = { const operations = {

View File

@ -0,0 +1,20 @@
export const loginMutation = /* GraphQL */`
mutation login($loginInput:CustomerUserAuthInfoInput!) {
account:createCustomerAuthTicket(customerUserAuthInfoInput:$loginInput) {
accessToken
userId
refreshToken
refreshTokenExpiration
accessTokenExpiration
customerAccount {
id
firstName
lastName
emailAddress
userName
}
}
}
`

View File

@ -0,0 +1,41 @@
const registerUserMutation = /* GraphQL */`
mutation registerUser($customerAccountInput: CustomerAccountInput!) {
account:createCustomerAccount(customerAccountInput:$customerAccountInput) {
emailAddress
userName
firstName
lastName
localeCode
userId
id
isAnonymous
attributes {
values
fullyQualifiedName
}
}
}`;
const registerUserLoginMutation = /* GraphQL */`
mutation registerUserLogin($accountId: Int!, $customerLoginInfoInput: CustomerLoginInfoInput!) {
account:createCustomerAccountLogin(accountId:$accountId, customerLoginInfoInput:$customerLoginInfoInput) {
accessToken
accessTokenExpiration
refreshToken
refreshTokenExpiration
userId
customerAccount {
id
emailAddress
firstName
userName
}
}
}`;
export {
registerUserMutation,
registerUserLoginMutation
};

View File

@ -0,0 +1,12 @@
export const getCustomerAccountQuery = /* GraphQL */`
query getUser {
customerAccount:getCurrentAccount {
id
firstName
lastName
emailAddress
userName
isAnonymous
}
}
`

View File

@ -8,9 +8,11 @@ const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher =
async (query: string, { variables, preview } = {}, fetchOptions) => { async (query: string, { variables, preview } = {}, fetchOptions) => {
const config = getConfig() const config = getConfig()
const res = await fetch(config.commerceUrl, { const res = await fetch(config.commerceUrl, {
//const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
...fetchOptions, ...fetchOptions,
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${config.apiToken}`,
...fetchOptions?.headers, ...fetchOptions?.headers,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -23,7 +25,7 @@ const fetchGraphqlApi: (getConfig: () => KiboCommerceConfig) => GraphQLFetcher =
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 KiboCommerce API' }],
status: res.status, status: res.status,
}) })
} }

View File

@ -1,16 +1,40 @@
import { MutationHook } from '@commerce/utils/types' import { MutationHook } from '@commerce/utils/types'
import useLogin, { UseLogin } from '@commerce/auth/use-login' import useLogin, { UseLogin } from '@commerce/auth/use-login'
import { useCallback } from 'react'
import { CommerceError } from '@commerce/utils/errors'
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/login',
method: 'POST'
}, },
async fetcher() { async fetcher({ input: { email, password }, options, fetch }) {
return null if (!(email && password)) {
throw new CommerceError({
message:
'An email 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]
)
}, },
} }

View File

@ -1,17 +1,26 @@
import { MutationHook } from '@commerce/utils/types' import { useCallback } from 'react'
import type { MutationHook } from '@commerce/utils/types'
import useLogout, { UseLogout } from '@commerce/auth/use-logout' import useLogout, { UseLogout } from '@commerce/auth/use-logout'
import type { LogoutHook } from '../types/logout'
import useCustomer from '../customer/use-customer'
export default useLogout as UseLogout<typeof handler> export default useLogout as UseLogout<typeof handler>
export const handler: MutationHook<any> = { export const handler: MutationHook<LogoutHook> = {
fetchOptions: { fetchOptions: {
query: '', url: '/api/logout',
method: 'GET',
}, },
async fetcher() { useHook: ({ fetch }) => () => {
return null const { mutate } = useCustomer()
return useCallback(
async function logout() {
const data = await fetch()
await mutate(null, false)
return data
},
[fetch, mutate]
)
}, },
useHook:
({ fetch }) =>
() =>
async () => {},
} }

View File

@ -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/signup',
method: 'POST',
}, },
async fetcher() { async fetcher({
return null input: { firstName, lastName, email, password },
options,
fetch,
}) {
if (!(firstName && lastName && email && password)) {
throw new CommerceError({
message:
'A first name, last name, email and password are required to signup',
})
}
return fetch({
...options,
body: { firstName, lastName, 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 }) =>
() =>
() => {},
} }

View File

@ -6,4 +6,4 @@
"search": true, "search": true,
"customerAuth": true "customerAuth": true
} }
} }

View File

@ -1,15 +1,24 @@
import { SWRHook } from '@commerce/utils/types' import { SWRHook } from '@commerce/utils/types'
import useCustomer, { UseCustomer } from '@commerce/customer/use-customer' import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
import type { CustomerHook } from '../types/customer'
export default useCustomer as UseCustomer<typeof handler> export default useCustomer as UseCustomer<typeof handler>
export const handler: SWRHook<any> = {
export const handler: SWRHook<CustomerHook> = {
fetchOptions: { fetchOptions: {
query: '', url: '/api/customer',
method: 'GET',
}, },
async fetcher({ input, options, fetch }) {}, async fetcher({ options, fetch }) {
useHook: () => () => { const data = await fetch(options)
return async function addItem() { return data?.customer ?? null
return {} },
} useHook: ({ useData }) => (input) => {
return useData({
swrOptions: {
revalidateOnFocus: false,
...input?.swrOptions,
},
})
}, },
} }

View File

@ -0,0 +1,8 @@
export function getCookieExpirationDate(maxAgeInDays: number){
const today = new Date();
const expirationDate = new Date();
const cookieExpirationDate = new Date ( expirationDate.setDate(today.getDate() + maxAgeInDays) )
return cookieExpirationDate;
}

View File

@ -0,0 +1,13 @@
export function prepareSetCookie(name: string, value: string, options: any = {}): string {
const cookieValue = [`${name}=${value}`];
if (options.maxAge) {
cookieValue.push(`Max-Age=${options.maxAge}`);
}
if (options.expires && !options.maxAge) {
cookieValue.push(`Expires=${options.expires.toUTCString()}`);
}
return cookieValue.join('; ');
}

View File

@ -0,0 +1,3 @@
export function setCookies(res: any, cookies: string[]): void {
res.setHeader('Set-Cookie', cookies);
}

View File

@ -0,0 +1,5 @@
import * as Core from '@commerce/types/customer'
export * from '@commerce/types/customer'
export type CustomerSchema = Core.CustomerSchema

View File

@ -0,0 +1,8 @@
import * as Core from '@commerce/types/login'
import type { CustomerUserAuthInfoInput } from '../schema'
export * from '@commerce/types/login'
export type LoginOperation = Core.LoginOperation & {
variables: CustomerUserAuthInfoInput
}

View File

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

View File

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