diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index e4cafdde7b..2f7a336487 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -548,7 +548,7 @@ export type ClientConfig = { authProviders: AuthProviders; debugMode: Scalars['Boolean']; signInPrefilled: Scalars['Boolean']; - supportChat: SupportChat; + support: Support; telemetry: Telemetry; }; @@ -1907,10 +1907,10 @@ export type StringNullableFilter = { startsWith?: InputMaybe; }; -export type SupportChat = { - __typename?: 'SupportChat'; +export type Support = { + __typename?: 'Support'; supportDriver: Scalars['String']; - supportFrontendKey?: Maybe; + supportFrontChatId?: Maybe; }; export type Telemetry = { @@ -1942,7 +1942,7 @@ export type User = { phoneNumber?: Maybe; settings: UserSettings; settingsId: Scalars['String']; - supportHMACKey?: Maybe; + supportUserHash: Scalars['String']; updatedAt: Scalars['DateTime']; workspaceMember?: Maybe; }; @@ -2437,7 +2437,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ refreshToken: Scalars['String']; @@ -2451,12 +2451,12 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, supportChat: { __typename?: 'SupportChat', supportDriver: string, supportFrontendKey?: string | null } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null } } }; export type GetCompaniesQueryVariables = Exact<{ orderBy?: InputMaybe | CompanyOrderByWithRelationInput>; @@ -2685,7 +2685,7 @@ export type SearchActivityQuery = { __typename?: 'Query', searchResults: Array<{ export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, supportHMACKey?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, locale: string, colorScheme: ColorScheme } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null, canImpersonate: boolean, supportUserHash: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, locale: string, colorScheme: ColorScheme } } }; export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>; @@ -3479,6 +3479,7 @@ export const VerifyDocument = gql` firstName lastName canImpersonate + supportUserHash workspaceMember { id allowImpersonation @@ -3587,6 +3588,7 @@ export const ImpersonateDocument = gql` firstName lastName canImpersonate + supportUserHash workspaceMember { id allowImpersonation @@ -3656,9 +3658,9 @@ export const GetClientConfigDocument = gql` enabled anonymizationEnabled } - supportChat { + support { supportDriver - supportFrontendKey + supportFrontChatId } } } @@ -4907,7 +4909,7 @@ export const GetCurrentUserDocument = gql` locale colorScheme } - supportHMACKey + supportUserHash } } `; diff --git a/front/src/modules/auth/queries/update.ts b/front/src/modules/auth/queries/update.ts index 674c0ff873..9ad644bf75 100644 --- a/front/src/modules/auth/queries/update.ts +++ b/front/src/modules/auth/queries/update.ts @@ -40,6 +40,7 @@ export const VERIFY = gql` firstName lastName canImpersonate + supportUserHash workspaceMember { id allowImpersonation @@ -99,6 +100,7 @@ export const IMPERSONATE = gql` firstName lastName canImpersonate + supportUserHash workspaceMember { id allowImpersonation diff --git a/front/src/modules/client-config/components/ClientConfigProvider.tsx b/front/src/modules/client-config/components/ClientConfigProvider.tsx index 8ddf943afb..20b0e009e8 100644 --- a/front/src/modules/client-config/components/ClientConfigProvider.tsx +++ b/front/src/modules/client-config/components/ClientConfigProvider.tsx @@ -33,7 +33,7 @@ export const ClientConfigProvider: React.FC = ({ setDebugMode(data?.clientConfig.debugMode); setSignInPrefilled(data?.clientConfig.signInPrefilled); setTelemetry(data?.clientConfig.telemetry); - setSupportChat(data?.clientConfig.supportChat); + setSupportChat(data?.clientConfig.support); } }, [ data, diff --git a/front/src/modules/client-config/queries/index.tsx b/front/src/modules/client-config/queries/index.tsx index 420962f27a..a5502c9017 100644 --- a/front/src/modules/client-config/queries/index.tsx +++ b/front/src/modules/client-config/queries/index.tsx @@ -13,9 +13,9 @@ export const GET_CLIENT_CONFIG = gql` enabled anonymizationEnabled } - supportChat { + support { supportDriver - supportFrontendKey + supportFrontChatId } } } diff --git a/front/src/modules/client-config/states/supportChatState.ts b/front/src/modules/client-config/states/supportChatState.ts index 5da5c79c02..8597704935 100644 --- a/front/src/modules/client-config/states/supportChatState.ts +++ b/front/src/modules/client-config/states/supportChatState.ts @@ -1,11 +1,11 @@ import { atom } from 'recoil'; -import { SupportChat } from '~/generated/graphql'; +import { Support } from '~/generated/graphql'; -export const supportChatState = atom({ +export const supportChatState = atom({ key: 'supportChatState', default: { - supportDriver: 'front', - supportFrontendKey: null, + supportDriver: 'none', + supportFrontChatId: null, }, }); diff --git a/front/src/modules/ui/button/components/Button.tsx b/front/src/modules/ui/button/components/Button.tsx index b55ac2cc3b..7f0add6ada 100644 --- a/front/src/modules/ui/button/components/Button.tsx +++ b/front/src/modules/ui/button/components/Button.tsx @@ -129,7 +129,7 @@ const StyledButton = styled.button< return theme.font.weight.medium; } }}; - gap: ${({ theme }) => theme.spacing(2)}; + gap: ${({ theme }) => theme.spacing(1)}; height: ${({ size }) => (size === 'small' ? '24px' : '32px')}; justify-content: flex-start; padding: ${({ theme, title }) => { diff --git a/front/src/modules/ui/icon/index.ts b/front/src/modules/ui/icon/index.ts index a13b69b26b..37cad46392 100644 --- a/front/src/modules/ui/icon/index.ts +++ b/front/src/modules/ui/icon/index.ts @@ -52,3 +52,4 @@ export { IconCalendar } from '@tabler/icons-react'; export { IconPencil } from '@tabler/icons-react'; export { IconCircleDot } from '@tabler/icons-react'; export { IconTag } from '@tabler/icons-react'; +export { IconHelpCircle } from '@tabler/icons-react'; diff --git a/front/src/modules/ui/navbar/components/SupportChat.tsx b/front/src/modules/ui/navbar/components/SupportChat.tsx index e77d4f244f..6d5dcf9afb 100644 --- a/front/src/modules/ui/navbar/components/SupportChat.tsx +++ b/front/src/modules/ui/navbar/components/SupportChat.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; @@ -9,24 +10,12 @@ import { ButtonSize, ButtonVariant, } from '@/ui/button/components/Button'; +import { IconHelpCircle } from '@/ui/icon'; const StyledButtonContainer = styled.div` display: flex; `; -const StyledQuestionMark = styled.div` - align-items: center; - border-radius: 50%; - border-style: solid; - border-width: ${({ theme }) => theme.spacing(0.25)}; - display: flex; - height: ${({ theme }) => theme.spacing(3.5)}; - justify-content: center; - margin-right: ${({ theme }) => theme.spacing(1)}; - width: ${({ theme }) => theme.spacing(3.5)}; -`; - -// insert a script tag into the DOM right before the closing body tag function insertScript({ src, innerHTML, @@ -63,41 +52,39 @@ function configureFront(chatId: string) { } export default function SupportChat() { + const theme = useTheme(); const user = useRecoilValue(currentUserState); const supportChatConfig = useRecoilValue(supportChatState); const [isFrontChatLoaded, setIsFrontChatLoaded] = useState(false); - const [isChatShowing, setIsChatShowing] = useState(false); useEffect(() => { if ( supportChatConfig?.supportDriver === 'front' && - supportChatConfig.supportFrontendKey && + supportChatConfig.supportFrontChatId && !isFrontChatLoaded ) { - configureFront(supportChatConfig.supportFrontendKey); + configureFront(supportChatConfig.supportFrontChatId); setIsFrontChatLoaded(true); } if (user?.email && isFrontChatLoaded) { window.FrontChat?.('identity', { email: user.email, name: user.displayName, - userHash: user?.supportHMACKey, + userHash: user?.supportUserHash, }); } }, [ isFrontChatLoaded, supportChatConfig?.supportDriver, - supportChatConfig.supportFrontendKey, + supportChatConfig.supportFrontChatId, user?.displayName, user?.email, - user?.supportHMACKey, + user?.supportUserHash, ]); function handleSupportClick() { if (supportChatConfig?.supportDriver === 'front') { - const action = isChatShowing ? 'hide' : 'show'; - setIsChatShowing(!isChatShowing); - window.FrontChat?.(action); + window.FrontChat?.('show'); } } @@ -107,7 +94,7 @@ export default function SupportChat() { variant={ButtonVariant.Tertiary} size={ButtonSize.Small} title="Support" - icon={?} + icon={} onClick={handleSupportClick} /> diff --git a/front/src/modules/users/queries/index.ts b/front/src/modules/users/queries/index.ts index a35d011e9a..69ee88640e 100644 --- a/front/src/modules/users/queries/index.ts +++ b/front/src/modules/users/queries/index.ts @@ -27,7 +27,7 @@ export const GET_CURRENT_USER = gql` locale colorScheme } - supportHMACKey + supportUserHash } } `; diff --git a/front/src/testing/graphqlMocks.ts b/front/src/testing/graphqlMocks.ts index 520f1dd246..fcce1c913d 100644 --- a/front/src/testing/graphqlMocks.ts +++ b/front/src/testing/graphqlMocks.ts @@ -207,9 +207,9 @@ export const graphqlMocks = [ debugMode: false, authProviders: { google: true, password: true, magicLink: false }, telemetry: { enabled: false, anonymizationEnabled: true }, - supportChat: { + support: { supportDriver: 'front', - supportFrontendKey: null, + supportFrontChatId: null, }, }, }), diff --git a/front/src/testing/mock-data/users.ts b/front/src/testing/mock-data/users.ts index 3e71dae0ca..24d4d9e7dd 100644 --- a/front/src/testing/mock-data/users.ts +++ b/front/src/testing/mock-data/users.ts @@ -17,6 +17,7 @@ export const mockedUsersData: Array = [ lastName: 'Test', avatarUrl: null, canImpersonate: false, + supportUserHash: '', workspaceMember: { __typename: 'WorkspaceMember', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', @@ -45,6 +46,7 @@ export const mockedUsersData: Array = [ firstName: 'Felix', lastName: 'Test', canImpersonate: false, + supportUserHash: '', workspaceMember: { __typename: 'WorkspaceMember', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', @@ -77,6 +79,7 @@ export const mockedOnboardingUsersData: Array = [ lastName: '', avatarUrl: null, canImpersonate: false, + supportUserHash: '', workspaceMember: { __typename: 'WorkspaceMember', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', @@ -106,6 +109,7 @@ export const mockedOnboardingUsersData: Array = [ lastName: '', avatarUrl: null, canImpersonate: false, + supportUserHash: '', workspaceMember: { __typename: 'WorkspaceMember', id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', diff --git a/server/.env.example b/server/.env.example index 13399c9b44..fc456d18b6 100644 --- a/server/.env.example +++ b/server/.env.example @@ -19,5 +19,5 @@ SIGN_IN_PREFILLED=true # STORAGE_TYPE=local # STORAGE_LOCAL_PATH=.local-storage # SUPPORT_DRIVER=front -# SUPPORT_HMAC_KEY=replace_me_with_a_random_string -# SUPPORT_FRONTEND_KEY=replace_me_with_a_random_string +# SUPPORT_FRONT_HMAC_KEY=replace_me_with_front_chat_verification_secret +# SUPPORT_FRONT_CHAT_ID=replace_me_with_front_chat_id diff --git a/server/src/core/client-config/client-config.entity.ts b/server/src/core/client-config/client-config.entity.ts index a590c886a9..079a76008e 100644 --- a/server/src/core/client-config/client-config.entity.ts +++ b/server/src/core/client-config/client-config.entity.ts @@ -22,12 +22,12 @@ class Telemetry { } @ObjectType() -class SupportChat { +class Support { @Field(() => String) supportDriver: string; @Field(() => String, { nullable: true }) - supportFrontendKey: string | null; + supportFrontChatId: string | undefined; } @ObjectType() @@ -44,6 +44,6 @@ export class ClientConfig { @Field(() => Boolean) debugMode: boolean; - @Field(() => SupportChat) - supportChat: SupportChat; + @Field(() => Support) + support: Support; } diff --git a/server/src/core/client-config/client-config.resolver.ts b/server/src/core/client-config/client-config.resolver.ts index f02b34b8f5..1f4d753ba1 100644 --- a/server/src/core/client-config/client-config.resolver.ts +++ b/server/src/core/client-config/client-config.resolver.ts @@ -23,9 +23,9 @@ export class ClientConfigResolver { }, signInPrefilled: this.environmentService.isSignInPrefilled(), debugMode: this.environmentService.isDebugMode(), - supportChat: { + support: { supportDriver: this.environmentService.getSupportDriver(), - supportFrontendKey: this.environmentService.getSupportFrontendKey(), + supportFrontChatId: this.environmentService.getSupportFrontChatId(), }, }; diff --git a/server/src/core/user/dto/user-with-HMAC.ts b/server/src/core/user/dto/user-with-HMAC.ts deleted file mode 100644 index 55d53ff3f1..0000000000 --- a/server/src/core/user/dto/user-with-HMAC.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Field, ObjectType } from '@nestjs/graphql'; - -import { User } from 'src/core/@generated/user/user.model'; - -@ObjectType() -export class UserWithHMACKey extends User { - @Field(() => String, { nullable: true }) - supportHMACKey: string | null; -} diff --git a/server/src/core/user/user.resolver.ts b/server/src/core/user/user.resolver.ts index 0dd1507b18..098d208f8b 100644 --- a/server/src/core/user/user.resolver.ts +++ b/server/src/core/user/user.resolver.ts @@ -43,7 +43,7 @@ import { EnvironmentService } from 'src/integrations/environment/environment.ser import { UserService } from './user.service'; -import { UserWithHMACKey } from './dto/user-with-HMAC'; +import { SupportDriver } from 'src/integrations/environment/interfaces/support.interface'; function getHMACKey(email?: string, key?: string | null) { if (!email || !key) return null; @@ -61,26 +61,25 @@ export class UserResolver { private environmentService: EnvironmentService, ) {} - @Query(() => UserWithHMACKey) + @Query(() => User) async currentUser( - @AuthUser() { id, email }: User, + @AuthUser() { id }: User, @PrismaSelector({ modelName: 'User' }) prismaSelect: PrismaSelect<'User'>, ) { - const key = this.environmentService.getSupportHMACKey(); + const select = prismaSelect.value; - delete select['supportHMACKey']; const user = await this.userService.findUnique({ where: { id, }, - select, + select }); assert(user, 'User not found'); - return { ...user, supportHMACKey: getHMACKey(email, key) }; + return user; } @UseFilters(ExceptionFilter) @@ -141,6 +140,17 @@ export class UserResolver { return `${parent.firstName ?? ''} ${parent.lastName ?? ''}`; } + @ResolveField(() => String, { + nullable: false, + }) + supportUserHash(@Parent() parent: User): string | null { + if (this.environmentService.getSupportDriver() !== SupportDriver.Front) { + return null; + } + const key = this.environmentService.getSupportFrontHMACKey(); + return getHMACKey(parent.email, key); + } + @Mutation(() => String) async uploadProfilePicture( @AuthUser() { id }: User, diff --git a/server/src/integrations/environment/environment.service.ts b/server/src/integrations/environment/environment.service.ts index 42dc99edbe..6147dbde9e 100644 --- a/server/src/integrations/environment/environment.service.ts +++ b/server/src/integrations/environment/environment.service.ts @@ -4,6 +4,7 @@ import { ConfigService } from '@nestjs/config'; import { AwsRegion } from './interfaces/aws-region.interface'; import { StorageType } from './interfaces/storage.interface'; +import { SupportDriver } from './interfaces/support.interface'; @Injectable() export class EnvironmentService { @@ -103,14 +104,14 @@ export class EnvironmentService { } getSupportDriver(): string { - return this.configService.get('SUPPORT_DRIVER') ?? 'front'; + return this.configService.get('SUPPORT_DRIVER') ?? SupportDriver.None; } - getSupportFrontendKey(): string | null { - return this.configService.get('SUPPORT_FRONTEND_KEY') ?? null; + getSupportFrontChatId(): string | undefined { + return this.configService.get('SUPPORT_FRONT_CHAT_ID'); } - getSupportHMACKey(): string | null { - return this.configService.get('SUPPORT_HMAC_KEY') ?? null; + getSupportFrontHMACKey(): string | undefined { + return this.configService.get('SUPPORT_FRONT_HMAC_KEY'); } } diff --git a/server/src/integrations/environment/environment.validation.ts b/server/src/integrations/environment/environment.validation.ts index d180861189..4da23a34e3 100644 --- a/server/src/integrations/environment/environment.validation.ts +++ b/server/src/integrations/environment/environment.validation.ts @@ -16,6 +16,7 @@ import { StorageType } from './interfaces/storage.interface'; import { AwsRegion } from './interfaces/aws-region.interface'; import { IsAWSRegion } from './decorators/is-aws-region.decorator'; import { CastToBoolean } from './decorators/cast-to-boolean.decorator'; +import { SupportDriver } from './interfaces/support.interface'; export class EnvironmentVariables { // Misc @@ -104,6 +105,19 @@ export class EnvironmentVariables { @IsString() @ValidateIf((env) => env.STORAGE_TYPE === StorageType.Local) STORAGE_LOCAL_PATH?: string; + + // Support + @IsEnum(SupportDriver) + @IsOptional() + SUPPORT_DRIVER?: SupportDriver; + + @ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front) + @IsString() + SUPPORT_FRONT_CHAT_ID?: AwsRegion; + + @ValidateIf((env) => env.SUPPORT_DRIVER === SupportDriver.Front) + @IsString() + SUPPORT_FRONT_HMAC_KEY?: string; } export function validate(config: Record) { diff --git a/server/src/integrations/environment/interfaces/support.interface.ts b/server/src/integrations/environment/interfaces/support.interface.ts new file mode 100644 index 0000000000..f5199e52d5 --- /dev/null +++ b/server/src/integrations/environment/interfaces/support.interface.ts @@ -0,0 +1,4 @@ +export enum SupportDriver { + None = 'none', + Front = 'front', +}