From 7adff0370f4db39884bb309184ec53373ac28f20 Mon Sep 17 00:00:00 2001 From: Elkin Urango Date: Mon, 4 Dec 2023 10:01:22 -0500 Subject: [PATCH] feat: add support for headless implementation --- graphql/quote.graphql | 6 + manifest.json | 3 + node/clients/vtexId.ts | 14 +- node/metrics/createQuote.ts | 2 +- node/resolvers/directives/withSession.ts | 27 ++- node/resolvers/mutations/index.ts | 221 +++++++++++++++++------ node/resolvers/queries/index.ts | 139 ++++++++------ 7 files changed, 292 insertions(+), 120 deletions(-) diff --git a/graphql/quote.graphql b/graphql/quote.graphql index 4ce5057..f7441ae 100644 --- a/graphql/quote.graphql +++ b/graphql/quote.graphql @@ -4,6 +4,9 @@ input QuoteInput { subtotal: Float note: String sendToSalesRep: Boolean + organization: String + costCenter: String + role: String } input QuoteItemInput { @@ -27,6 +30,9 @@ input QuoteUpdateInput { note: String decline: Boolean expirationDate: String + organization: String + costCenter: String + role: String } type Quotes { diff --git a/manifest.json b/manifest.json index ccd7775..bc98f65 100644 --- a/manifest.json +++ b/manifest.json @@ -62,6 +62,9 @@ { "name": "send-message" }, + { + "name": "sphinx-is-admin" + }, { "name": "outbound-access", "attrs": { diff --git a/node/clients/vtexId.ts b/node/clients/vtexId.ts index dbf4b52..0e848ff 100644 --- a/node/clients/vtexId.ts +++ b/node/clients/vtexId.ts @@ -1,19 +1,25 @@ import type { InstanceOptions, IOContext } from '@vtex/api' -import { ExternalClient } from '@vtex/api' +import { JanusClient } from '@vtex/api' interface AuthenticatedUser { user: string } -export default class VtexId extends ExternalClient { +export default class VtexId extends JanusClient { constructor(context: IOContext, options?: InstanceOptions) { - super('http://vtexid.vtex.com.br/api/', context, options) + super(context, { + ...options, + headers: { + ...options?.headers, + 'x-vtex-user-agent': context.userAgent, + }, + }) } public async getAuthenticatedUser( authToken: string ): Promise { - return this.http.get('vtexid/pub/authenticated/user/', { + return this.http.get('/api/vtexid/pub/authenticated/user/', { metric: 'authenticated-user-get', params: { authToken }, }) diff --git a/node/metrics/createQuote.ts b/node/metrics/createQuote.ts index b421241..35c6564 100644 --- a/node/metrics/createQuote.ts +++ b/node/metrics/createQuote.ts @@ -57,7 +57,7 @@ export class CreateQuoteMetric implements Metric { const buildQuoteMetric = ( metricsParam: CreateQuoteMetricParam ): CreateQuoteMetric => { - const { namespaces } = metricsParam.sessionData + const { namespaces } = metricsParam.sessionData || {} const accountName = namespaces?.account?.accountName?.value const userEmail = namespaces?.profile?.email?.value diff --git a/node/resolvers/directives/withSession.ts b/node/resolvers/directives/withSession.ts index c6d621f..4f871fd 100644 --- a/node/resolvers/directives/withSession.ts +++ b/node/resolvers/directives/withSession.ts @@ -10,16 +10,29 @@ export class WithSession extends SchemaDirectiveVisitor { field.resolve = async (root: any, args: any, context: any, info: any) => { const { - clients: { session }, + clients: { session, vtexId }, vtex: { sessionToken }, } = context - context.vtex.sessionData = await session - .getSession(sessionToken as string, ['*']) - .then((currentSession: any) => { - return currentSession.sessionData - }) - .catch(() => null) + const token = context.request.header?.vtexidclientauthcookie + + if (sessionToken) { + context.vtex.sessionData = await session + .getSession(sessionToken as string, ['*']) + .then((currentSession: any) => { + return currentSession.sessionData + }) + .catch(() => null) + } else if (token) { + const authenticatedUser = await vtexId.getAuthenticatedUser(token) + + if (authenticatedUser?.userId) { + context.vtex.authenticatedUser = { + ...authenticatedUser, + token, + } + } + } return resolve(root, args, context, info) } diff --git a/node/resolvers/mutations/index.ts b/node/resolvers/mutations/index.ts index 61451a0..a9a00c1 100644 --- a/node/resolvers/mutations/index.ts +++ b/node/resolvers/mutations/index.ts @@ -1,4 +1,5 @@ import { indexBy, map, prop } from 'ramda' +import { UserInputError } from '@vtex/api' import { checkConfig, @@ -53,7 +54,16 @@ export const Mutation = { createQuote: async ( _: any, { - input: { referenceName, items, subtotal, note, sendToSalesRep }, + input: { + referenceName, + items, + subtotal, + note, + sendToSalesRep, + organization, + costCenter, + role: roleName, + }, }: { input: { referenceName: string @@ -61,6 +71,9 @@ export const Mutation = { subtotal: number note: string sendToSalesRep: boolean + organization?: string + costCenter?: string + role: string } }, ctx: Context @@ -71,25 +84,58 @@ export const Mutation = { vtex: { logger }, } = ctx - const { sessionData, storefrontPermissions, segmentData } = vtex as any + const { + sessionData, + storefrontPermissions, + segmentData, + authenticatedUser, + } = vtex as any + + const isAdmin = !!( + authenticatedUser || + (sessionData?.namespaces?.authentication?.adminUserEmail?.value && + !storefrontPermissions?.permissions?.length) + ) const settings = await checkConfig(ctx) - checkSession(sessionData) + if (!isAdmin) { + checkSession(sessionData) + } - if (!storefrontPermissions?.permissions?.includes('create-quotes')) { + if ( + !storefrontPermissions?.permissions?.includes('create-quotes') && + !isAdmin + ) { throw new GraphQLError('operation-not-permitted') } - const email = sessionData.namespaces.profile.email.value - const { - role: { slug }, - } = storefrontPermissions + if (isAdmin) { + if (!organization || !costCenter) { + throw new UserInputError( + 'organizationId and costcenterId are required.' + ) + } + } - const { - organization: { value: organizationId }, - costcenter: { value: costCenterId }, - } = sessionData.namespaces['storefront-permissions'] + const email = + sessionData?.namespaces?.profile?.email?.value || + authenticatedUser?.user || + sessionData?.namespaces?.authentication?.adminUserEmail?.value + + const { role } = storefrontPermissions || {} + + const { slug } = role || {} + + const newRole = isAdmin ? roleName || 'admin' : slug + + const organizationId = + sessionData?.namespaces?.['storefront-permissions']?.organization + ?.value || organization + + const costCenterId = + sessionData?.namespaces?.['storefront-permissions']?.costcenter?.value || + costCenter const now = new Date() const nowISO = now.toISOString() @@ -107,7 +153,7 @@ export const Mutation = { date: nowISO, email, note, - role: slug, + role: newRole, status, }, ] @@ -118,7 +164,7 @@ export const Mutation = { costCenter: costCenterId, creationDate: nowISO, creatorEmail: email, - creatorRole: slug, + creatorRole: newRole, expirationDate: expirationDateISO, items, lastUpdate, @@ -166,7 +212,7 @@ export const Mutation = { userData: { orgId: organizationId, costId: costCenterId, - roleId: slug, + roleId: newRole, }, costCenterName: 'costCenterData?.getCostCenterById?.name', buyerOrgName: 'organizationData?.getOrganizationById?.name', @@ -196,7 +242,17 @@ export const Mutation = { updateQuote: async ( _: any, { - input: { id, items, subtotal, note, decline, expirationDate }, + input: { + id, + items, + subtotal, + note, + decline, + expirationDate, + organization, + costCenter, + role: roleName + }, }: { input: { id: string @@ -205,6 +261,9 @@ export const Mutation = { note: string decline: boolean expirationDate: string + organization: string + costCenter: string + role: string } }, ctx: Context @@ -215,30 +274,58 @@ export const Mutation = { vtex: { logger }, } = ctx - const { sessionData, storefrontPermissions } = vtex as any + const { + sessionData, + storefrontPermissions, + authenticatedUser, + } = vtex as any + + const isAdmin = !!( + authenticatedUser || + (sessionData?.namespaces?.authentication?.adminUserEmail?.value && + !storefrontPermissions?.permissions?.length) + ) - checkSession(sessionData) + if (!isAdmin) { + checkSession(sessionData) + } - const email = sessionData.namespaces.profile.email.value - const { - permissions, - role: { slug }, - } = storefrontPermissions + if (isAdmin) { + if (!organization || !costCenter) { + throw new UserInputError( + 'organizationId and costcenterId are required.' + ) + } + } + + const email = + sessionData?.namespaces?.profile?.email?.value || + authenticatedUser?.user || + sessionData?.namespaces?.authentication?.adminUserEmail?.value + + const { permissions, role } = storefrontPermissions || {} - const isCustomer = slug.includes('customer') - const isSales = slug.includes('sales') + const { slug } = role || {} + + const isCustomer = slug?.includes('customer') + const isSales = slug?.includes('sales') const itemsChanged = items?.length > 0 - checkPermissionsForUpdateQuote({ - permissions, - itemsChanged, - decline, - }) + if (!isAdmin) { + checkPermissionsForUpdateQuote({ + permissions, + itemsChanged, + decline, + }) + } - const { - organization: { value: userOrganizationId }, - costcenter: { value: userCostCenterId }, - } = sessionData.namespaces['storefront-permissions'] + const userOrganizationId = + sessionData?.namespaces?.['storefront-permissions']?.organization + ?.value || organization + + const userCostCenterId = + sessionData?.namespaces?.['storefront-permissions']?.costcenter?.value || + costCenter const now = new Date() const nowISO = now.toISOString() @@ -254,15 +341,25 @@ export const Mutation = { const expirationChanged = expirationDate !== existingQuote.expirationDate - checkOperationsForUpdateQuote({ - permissions, - expirationChanged, - itemsChanged, - existingQuote, - userCostCenterId, - userOrganizationId, - declineQuote: decline, - }) + if (!isAdmin) { + checkOperationsForUpdateQuote({ + permissions, + expirationChanged, + itemsChanged, + existingQuote, + userCostCenterId, + userOrganizationId, + declineQuote: decline, + }) + } else { + if (userOrganizationId !== existingQuote.organization) { + throw new GraphQLError('operation-not-permitted') + } + + if (userCostCenterId !== existingQuote.costCenter) { + throw new GraphQLError('operation-not-permitted') + } + } const readyOrRevised = itemsChanged ? 'ready' : 'revised' const status = decline ? 'declined' : readyOrRevised @@ -272,7 +369,7 @@ export const Mutation = { date: nowISO, email, note, - role: slug, + role: isAdmin ? roleName || 'admin' : slug, status, } @@ -285,7 +382,7 @@ export const Mutation = { expirationDate: expirationChanged ? expirationDate : existingQuote.expirationDate, - items: itemsChanged ? items : existingQuote.items, + items: itemsChanged ? items : existingQuote?.items || [], lastUpdate, status, subtotal: subtotal ?? existingQuote.subtotal, @@ -329,7 +426,7 @@ export const Mutation = { }) }) - return data.id + return data?.id || existingQuote.id } catch (error) { logger.warn({ error, @@ -349,17 +446,31 @@ export const Mutation = { vtex: { account, logger }, } = ctx - const { sessionData, storefrontPermissions } = vtex as any + const { + sessionData, + storefrontPermissions, + authenticatedUser, + } = vtex as any + + const isAdmin = !!( + authenticatedUser || + (sessionData?.namespaces?.authentication?.adminUserEmail?.value && + !storefrontPermissions?.permissions?.length) + ) - checkSession(sessionData) + if (!isAdmin) { + checkSession(sessionData) + } - const { permissions } = storefrontPermissions + const { permissions } = storefrontPermissions || {} - if (!permissions.includes('use-quotes')) { + if (!permissions?.includes('use-quotes') && !isAdmin) { throw new GraphQLError('operation-not-permitted') } - const token = ctx.cookies.get(`VtexIdclientAutCookie_${account}`) + const token = + ctx.cookies.get(`VtexIdclientAutCookie_${account}`) || + authenticatedUser?.token const useHeaders = { 'Content-Type': 'application/json', @@ -462,8 +573,8 @@ export const Mutation = { itemsAdded.forEach((item: any, key: number) => { orderItems.push({ index: key, - price: prop(item.id, sellingPriceMap).price, - quantity: null, + price: prop(item.id, sellingPriceMap)?.price, + quantity: item.quantity, }) }) @@ -479,7 +590,9 @@ export const Mutation = { quote, orderFormId, account, - userEmail: sessionData?.namespaces?.profile?.email?.value, + userEmail: + sessionData?.namespaces?.profile?.email?.value || + authenticatedUser?.user, } sendUseQuoteMetric(metricParams) diff --git a/node/resolvers/queries/index.ts b/node/resolvers/queries/index.ts index c2764fa..35d719b 100644 --- a/node/resolvers/queries/index.ts +++ b/node/resolvers/queries/index.ts @@ -1,3 +1,5 @@ +import { UserInputError } from '@vtex/api' + import { checkConfig } from '../utils/checkConfig' import GraphQLError from '../../utils/GraphQLError' import { @@ -16,6 +18,7 @@ const buildWhereStatement = async ({ userOrganizationId, userCostCenterId, userSalesChannel, + isAdmin, }: { permissions: string[] organization?: string[] @@ -25,12 +28,13 @@ const buildWhereStatement = async ({ userOrganizationId: string userCostCenterId: string userSalesChannel?: string + isAdmin?: boolean }) => { const whereArray = [] // if user only has permission to access their organization's quotes, // hard-code that organization into the masterdata search - if (!permissions.includes('access-quotes-all')) { + if (!permissions?.includes('access-quotes-all') && !isAdmin) { whereArray.push(`organization=${userOrganizationId}`) } else if (organization?.length) { const orgArray = organization.map((org) => `organization=${org}`) @@ -42,8 +46,9 @@ const buildWhereStatement = async ({ // similarly, if user only has permission to see their cost center's quotes, // hard-code its ID into the search if ( - !permissions.includes('access-quotes-all') && - !permissions.includes('access-quotes-organization') + !permissions?.includes('access-quotes-all') && + !permissions?.includes('access-quotes-organization') && + !isAdmin ) { whereArray.push(`costCenter=${userCostCenterId}`) } else if (costCenter?.length) { @@ -57,9 +62,10 @@ const buildWhereStatement = async ({ // hard-code its value into the search // allow all users to view quotes without a sales channel if ( - !permissions.includes('access-quotes-all') && - !permissions.includes('access-quotes-all-saleschannel') && - userSalesChannel + !permissions?.includes('access-quotes-all') && + !permissions?.includes('access-quotes-all-saleschannel') && + userSalesChannel && + !isAdmin ) { whereArray.push( `((salesChannel is null) OR salesChannel="${userSalesChannel}")` @@ -94,29 +100,37 @@ export const Query = { vtex: { logger }, } = ctx - const { sessionData, storefrontPermissions, segmentData } = vtex as any + const { + sessionData, + storefrontPermissions, + segmentData, + authenticatedUser, + } = vtex as any + + const isAdmin = !!( + authenticatedUser || + (sessionData?.namespaces?.authentication?.adminUserEmail?.value && + !storefrontPermissions?.permissions?.length) + ) - if ( - !storefrontPermissions?.permissions?.length || - !sessionData?.namespaces['storefront-permissions']?.organization?.value || - !sessionData?.namespaces['storefront-permissions']?.costcenter?.value - ) { + if (!storefrontPermissions?.permissions?.length && !isAdmin) { return null } - const { permissions } = storefrontPermissions + const { permissions } = storefrontPermissions || {} const userOrganizationId = - sessionData.namespaces['storefront-permissions'].organization.value + sessionData?.namespaces?.['storefront-permissions']?.organization?.value const userCostCenterId = - sessionData.namespaces['storefront-permissions'].costcenter.value + sessionData?.namespaces?.['storefront-permissions']?.costcenter?.value const userSalesChannel = segmentData?.channel if ( - !permissions.some( + !permissions?.some( (permission: string) => permission.indexOf('access-quotes') >= 0 - ) + ) && + !isAdmin ) { return null } @@ -130,32 +144,34 @@ export const Query = { id, }) - // if user only has permission to view their organization's quotes, check that the org matches - if ( - !permissions.includes('access-quotes-all') && - permissions.includes('access-quotes-organization') && - userOrganizationId !== quote.organization - ) { - return null - } - - // if user only has permission to view their cost center's quotes, check that the cost center matches - if ( - !permissions.includes('access-quotes-all') && - !permissions.includes('access-quotes-organization') && - userCostCenterId !== quote.costCenter - ) { - return null - } - - // if user only has permission to view quotes from their sales channel, check that the sales channel matches or is null - if ( - !permissions.includes('access-quotes-all') && - !permissions.includes('access-quotes-all-saleschannel') && - quote.salesChannel && - userSalesChannel !== quote.salesChannel - ) { - return null + if (!isAdmin) { + // if user only has permission to view their organization's quotes, check that the org matches + if ( + !permissions.includes('access-quotes-all') && + permissions.includes('access-quotes-organization') && + userOrganizationId !== quote.organization + ) { + return null + } + + // if user only has permission to view their cost center's quotes, check that the cost center matches + if ( + !permissions.includes('access-quotes-all') && + !permissions.includes('access-quotes-organization') && + userCostCenterId !== quote.costCenter + ) { + return null + } + + // if user only has permission to view quotes from their sales channel, check that the sales channel matches or is null + if ( + !permissions.includes('access-quotes-all') && + !permissions.includes('access-quotes-all-saleschannel') && + quote.salesChannel && + userSalesChannel !== quote.salesChannel + ) { + return null + } } return quote @@ -202,27 +218,41 @@ export const Query = { vtex: { logger }, } = ctx - const { sessionData, storefrontPermissions, segmentData } = vtex as any + const { + sessionData, + storefrontPermissions, + segmentData, + authenticatedUser, + } = vtex as any + + const isAdmin = !!( + authenticatedUser || + (sessionData?.namespaces?.authentication?.adminUserEmail?.value && + !storefrontPermissions?.permissions?.length) + ) - if ( - !storefrontPermissions?.permissions?.length || - !sessionData?.namespaces['storefront-permissions']?.organization?.value || - !sessionData?.namespaces['storefront-permissions']?.costcenter?.value - ) { + if (!sessionData || !storefrontPermissions?.permissions?.length) { + if (!organization?.length) { + throw new UserInputError('organization is required.') + } + } + + if (!storefrontPermissions?.permissions?.length && !isAdmin) { return null } - const { permissions } = storefrontPermissions + const { permissions } = storefrontPermissions || {} const userOrganizationId = - sessionData.namespaces['storefront-permissions'].organization.value + sessionData?.namespaces?.['storefront-permissions']?.organization?.value const userCostCenterId = - sessionData.namespaces['storefront-permissions'].costcenter.value + sessionData?.namespaces?.['storefront-permissions']?.costcenter?.value if ( - !permissions.some( + !permissions?.some( (permission: string) => permission.indexOf('access-quotes') >= 0 - ) + ) && + !isAdmin ) { return null } @@ -240,6 +270,7 @@ export const Query = { userOrganizationId, userCostCenterId, userSalesChannel, + isAdmin, }) try {