Add support chat (#1066)
* Add support chat Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Refactor the chat logic Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add HMAC signing Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Update the button styles Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Update the button styles Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Refactor the chat logic Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Fix the chat not loading Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Fix the chat not loading Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add requested changes Co-authored-by: v1b3m <vibenjamin6@gmail.com> --------- Co-authored-by: v1b3m <vibenjamin6@gmail.com>
This commit is contained in:
@ -548,6 +548,7 @@ export type ClientConfig = {
|
|||||||
authProviders: AuthProviders;
|
authProviders: AuthProviders;
|
||||||
debugMode: Scalars['Boolean'];
|
debugMode: Scalars['Boolean'];
|
||||||
signInPrefilled: Scalars['Boolean'];
|
signInPrefilled: Scalars['Boolean'];
|
||||||
|
supportChat: SupportChat;
|
||||||
telemetry: Telemetry;
|
telemetry: Telemetry;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1906,6 +1907,12 @@ export type StringNullableFilter = {
|
|||||||
startsWith?: InputMaybe<Scalars['String']>;
|
startsWith?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SupportChat = {
|
||||||
|
__typename?: 'SupportChat';
|
||||||
|
supportDriver: Scalars['String'];
|
||||||
|
supportFrontendKey?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
export type Telemetry = {
|
export type Telemetry = {
|
||||||
__typename?: 'Telemetry';
|
__typename?: 'Telemetry';
|
||||||
anonymizationEnabled: Scalars['Boolean'];
|
anonymizationEnabled: Scalars['Boolean'];
|
||||||
@ -1935,6 +1942,7 @@ export type User = {
|
|||||||
phoneNumber?: Maybe<Scalars['String']>;
|
phoneNumber?: Maybe<Scalars['String']>;
|
||||||
settings: UserSettings;
|
settings: UserSettings;
|
||||||
settingsId: Scalars['String'];
|
settingsId: Scalars['String'];
|
||||||
|
supportHMACKey?: Maybe<Scalars['String']>;
|
||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
workspaceMember?: Maybe<WorkspaceMember>;
|
workspaceMember?: Maybe<WorkspaceMember>;
|
||||||
};
|
};
|
||||||
@ -2448,7 +2456,7 @@ export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __ty
|
|||||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
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 } } };
|
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 GetCompaniesQueryVariables = Exact<{
|
export type GetCompaniesQueryVariables = Exact<{
|
||||||
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
|
||||||
@ -2677,7 +2685,7 @@ export type SearchActivityQuery = { __typename?: 'Query', searchResults: Array<{
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
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, 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, 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 GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -3648,6 +3656,10 @@ export const GetClientConfigDocument = gql`
|
|||||||
enabled
|
enabled
|
||||||
anonymizationEnabled
|
anonymizationEnabled
|
||||||
}
|
}
|
||||||
|
supportChat {
|
||||||
|
supportDriver
|
||||||
|
supportFrontendKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -4895,6 +4907,7 @@ export const GetCurrentUserDocument = gql`
|
|||||||
locale
|
locale
|
||||||
colorScheme
|
colorScheme
|
||||||
}
|
}
|
||||||
|
supportHMACKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
||||||
|
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||||
import { telemetryState } from '@/client-config/states/telemetryState';
|
import { telemetryState } from '@/client-config/states/telemetryState';
|
||||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [, setSignInPrefilled] = useRecoilState(isSignInPrefilledState);
|
const [, setSignInPrefilled] = useRecoilState(isSignInPrefilledState);
|
||||||
const [, setTelemetry] = useRecoilState(telemetryState);
|
const [, setTelemetry] = useRecoilState(telemetryState);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const setSupportChat = useSetRecoilState(supportChatState);
|
||||||
|
|
||||||
const { data, loading } = useGetClientConfigQuery();
|
const { data, loading } = useGetClientConfigQuery();
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setDebugMode(data?.clientConfig.debugMode);
|
setDebugMode(data?.clientConfig.debugMode);
|
||||||
setSignInPrefilled(data?.clientConfig.signInPrefilled);
|
setSignInPrefilled(data?.clientConfig.signInPrefilled);
|
||||||
setTelemetry(data?.clientConfig.telemetry);
|
setTelemetry(data?.clientConfig.telemetry);
|
||||||
|
setSupportChat(data?.clientConfig.supportChat);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
data,
|
data,
|
||||||
@ -40,6 +43,7 @@ export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setTelemetry,
|
setTelemetry,
|
||||||
setIsLoading,
|
setIsLoading,
|
||||||
loading,
|
loading,
|
||||||
|
setSupportChat,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return isLoading ? <></> : <>{children}</>;
|
return isLoading ? <></> : <>{children}</>;
|
||||||
|
|||||||
@ -13,6 +13,10 @@ export const GET_CLIENT_CONFIG = gql`
|
|||||||
enabled
|
enabled
|
||||||
anonymizationEnabled
|
anonymizationEnabled
|
||||||
}
|
}
|
||||||
|
supportChat {
|
||||||
|
supportDriver
|
||||||
|
supportFrontendKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
11
front/src/modules/client-config/states/supportChatState.ts
Normal file
11
front/src/modules/client-config/states/supportChatState.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
import { SupportChat } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const supportChatState = atom<SupportChat>({
|
||||||
|
key: 'supportChatState',
|
||||||
|
default: {
|
||||||
|
supportDriver: 'front',
|
||||||
|
supportFrontendKey: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
3
front/src/modules/types/custom.d.ts
vendored
Normal file
3
front/src/modules/types/custom.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
declare interface Window {
|
||||||
|
FrontChat?: (method: string, ...args: any[]) => void;
|
||||||
|
}
|
||||||
@ -2,20 +2,29 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import NavItemsContainer from './NavItemsContainer';
|
import NavItemsContainer from './NavItemsContainer';
|
||||||
import NavWorkspaceButton from './NavWorkspaceButton';
|
import NavWorkspaceButton from './NavWorkspaceButton';
|
||||||
|
import SupportChat from './SupportChat';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(2.5)};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default function MainNavbar({ children }: OwnProps) {
|
export default function MainNavbar({ children }: OwnProps) {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<NavWorkspaceButton />
|
<div>
|
||||||
<NavItemsContainer>{children}</NavItemsContainer>
|
<NavWorkspaceButton />
|
||||||
|
<NavItemsContainer>{children}</NavItemsContainer>
|
||||||
|
</div>
|
||||||
|
<SupportChat />
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
115
front/src/modules/ui/navbar/components/SupportChat.tsx
Normal file
115
front/src/modules/ui/navbar/components/SupportChat.tsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonSize,
|
||||||
|
ButtonVariant,
|
||||||
|
} from '@/ui/button/components/Button';
|
||||||
|
|
||||||
|
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,
|
||||||
|
onLoad,
|
||||||
|
}: {
|
||||||
|
src?: string;
|
||||||
|
innerHTML?: string;
|
||||||
|
onLoad?: (...args: any[]) => void;
|
||||||
|
}) {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
if (src) script.src = src;
|
||||||
|
if (innerHTML) script.innerHTML = innerHTML;
|
||||||
|
if (onLoad) script.onload = onLoad;
|
||||||
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureFront(chatId: string) {
|
||||||
|
const url = 'https://chat-assets.frontapp.com/v1/chat.bundle.js';
|
||||||
|
// check if Front Chat script is already loaded
|
||||||
|
const script = document.querySelector(`script[src="${url}"]`);
|
||||||
|
|
||||||
|
if (!script) {
|
||||||
|
// insert script and initialize Front Chat when it loads
|
||||||
|
insertScript({
|
||||||
|
src: url,
|
||||||
|
onLoad: () => {
|
||||||
|
window.FrontChat?.('init', {
|
||||||
|
chatId,
|
||||||
|
useDefaultLauncher: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SupportChat() {
|
||||||
|
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 &&
|
||||||
|
!isFrontChatLoaded
|
||||||
|
) {
|
||||||
|
configureFront(supportChatConfig.supportFrontendKey);
|
||||||
|
setIsFrontChatLoaded(true);
|
||||||
|
}
|
||||||
|
if (user?.email && isFrontChatLoaded) {
|
||||||
|
window.FrontChat?.('identity', {
|
||||||
|
email: user.email,
|
||||||
|
name: user.displayName,
|
||||||
|
userHash: user?.supportHMACKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isFrontChatLoaded,
|
||||||
|
supportChatConfig?.supportDriver,
|
||||||
|
supportChatConfig.supportFrontendKey,
|
||||||
|
user?.displayName,
|
||||||
|
user?.email,
|
||||||
|
user?.supportHMACKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
function handleSupportClick() {
|
||||||
|
if (supportChatConfig?.supportDriver === 'front') {
|
||||||
|
const action = isChatShowing ? 'hide' : 'show';
|
||||||
|
setIsChatShowing(!isChatShowing);
|
||||||
|
window.FrontChat?.(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFrontChatLoaded ? (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.Tertiary}
|
||||||
|
size={ButtonSize.Small}
|
||||||
|
title="Support"
|
||||||
|
icon={<StyledQuestionMark>?</StyledQuestionMark>}
|
||||||
|
onClick={handleSupportClick}
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
@ -27,6 +27,7 @@ export const GET_CURRENT_USER = gql`
|
|||||||
locale
|
locale
|
||||||
colorScheme
|
colorScheme
|
||||||
}
|
}
|
||||||
|
supportHMACKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@ -207,6 +207,10 @@ export const graphqlMocks = [
|
|||||||
debugMode: false,
|
debugMode: false,
|
||||||
authProviders: { google: true, password: true, magicLink: false },
|
authProviders: { google: true, password: true, magicLink: false },
|
||||||
telemetry: { enabled: false, anonymizationEnabled: true },
|
telemetry: { enabled: false, anonymizationEnabled: true },
|
||||||
|
supportChat: {
|
||||||
|
supportDriver: 'front',
|
||||||
|
supportFrontendKey: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -17,4 +17,7 @@ SIGN_IN_PREFILLED=true
|
|||||||
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
|
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
|
||||||
# AUTH_GOOGLE_ENABLED=false
|
# AUTH_GOOGLE_ENABLED=false
|
||||||
# STORAGE_TYPE=local
|
# STORAGE_TYPE=local
|
||||||
# STORAGE_LOCAL_PATH=.local-storage
|
# 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
|
||||||
|
|||||||
@ -21,6 +21,15 @@ class Telemetry {
|
|||||||
anonymizationEnabled: boolean;
|
anonymizationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
class SupportChat {
|
||||||
|
@Field(() => String)
|
||||||
|
supportDriver: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
supportFrontendKey: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ClientConfig {
|
export class ClientConfig {
|
||||||
@Field(() => AuthProviders, { nullable: false })
|
@Field(() => AuthProviders, { nullable: false })
|
||||||
@ -34,4 +43,7 @@ export class ClientConfig {
|
|||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
debugMode: boolean;
|
debugMode: boolean;
|
||||||
|
|
||||||
|
@Field(() => SupportChat)
|
||||||
|
supportChat: SupportChat;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,10 @@ export class ClientConfigResolver {
|
|||||||
},
|
},
|
||||||
signInPrefilled: this.environmentService.isSignInPrefilled(),
|
signInPrefilled: this.environmentService.isSignInPrefilled(),
|
||||||
debugMode: this.environmentService.isDebugMode(),
|
debugMode: this.environmentService.isDebugMode(),
|
||||||
|
supportChat: {
|
||||||
|
supportDriver: this.environmentService.getSupportDriver(),
|
||||||
|
supportFrontendKey: this.environmentService.getSupportFrontendKey(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve(clientConfig);
|
return Promise.resolve(clientConfig);
|
||||||
|
|||||||
9
server/src/core/user/dto/user-with-HMAC.ts
Normal file
9
server/src/core/user/dto/user-with-HMAC.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@ -2,12 +2,13 @@ import { Module } from '@nestjs/common';
|
|||||||
|
|
||||||
import { FileModule } from 'src/core/file/file.module';
|
import { FileModule } from 'src/core/file/file.module';
|
||||||
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
|
import { WorkspaceModule } from 'src/core/workspace/workspace.module';
|
||||||
|
import { EnvironmentModule } from 'src/integrations/environment/environment.module';
|
||||||
|
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
import { UserResolver } from './user.resolver';
|
import { UserResolver } from './user.resolver';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [FileModule, WorkspaceModule],
|
imports: [FileModule, WorkspaceModule, EnvironmentModule],
|
||||||
providers: [UserService, UserResolver],
|
providers: [UserService, UserResolver],
|
||||||
exports: [UserService],
|
exports: [UserService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||||||
|
|
||||||
import { AbilityFactory } from 'src/ability/ability.factory';
|
import { AbilityFactory } from 'src/ability/ability.factory';
|
||||||
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||||
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
import { UserResolver } from './user.resolver';
|
import { UserResolver } from './user.resolver';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
@ -25,6 +26,10 @@ describe('UserResolver', () => {
|
|||||||
provide: FileUploadService,
|
provide: FileUploadService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: EnvironmentService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
|
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import { accessibleBy } from '@casl/prisma';
|
import { accessibleBy } from '@casl/prisma';
|
||||||
import { Prisma, Workspace } from '@prisma/client';
|
import { Prisma, Workspace } from '@prisma/client';
|
||||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||||
@ -37,32 +39,48 @@ import { UpdateOneUserArgs } from 'src/core/@generated/user/update-one-user.args
|
|||||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||||
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||||
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||||
|
import { EnvironmentService } from 'src/integrations/environment/environment.service';
|
||||||
|
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
import { UserWithHMACKey } from './dto/user-with-HMAC';
|
||||||
|
|
||||||
|
function getHMACKey(email?: string, key?: string | null) {
|
||||||
|
if (!email || !key) return null;
|
||||||
|
|
||||||
|
const hmac = crypto.createHmac('sha256', key);
|
||||||
|
return hmac.update(email).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver(() => User)
|
@Resolver(() => User)
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly userService: UserService,
|
private readonly userService: UserService,
|
||||||
private readonly fileUploadService: FileUploadService,
|
private readonly fileUploadService: FileUploadService,
|
||||||
|
private environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => User)
|
@Query(() => UserWithHMACKey)
|
||||||
async currentUser(
|
async currentUser(
|
||||||
@AuthUser() { id }: User,
|
@AuthUser() { id, email }: User,
|
||||||
@PrismaSelector({ modelName: 'User' })
|
@PrismaSelector({ modelName: 'User' })
|
||||||
prismaSelect: PrismaSelect<'User'>,
|
prismaSelect: PrismaSelect<'User'>,
|
||||||
) {
|
) {
|
||||||
|
const key = this.environmentService.getSupportHMACKey();
|
||||||
|
|
||||||
|
const select = prismaSelect.value;
|
||||||
|
delete select['supportHMACKey'];
|
||||||
|
|
||||||
const user = await this.userService.findUnique({
|
const user = await this.userService.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
select: prismaSelect.value,
|
select,
|
||||||
});
|
});
|
||||||
assert(user, 'User not found');
|
assert(user, 'User not found');
|
||||||
|
|
||||||
return user;
|
return { ...user, supportHMACKey: getHMACKey(email, key) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseFilters(ExceptionFilter)
|
@UseFilters(ExceptionFilter)
|
||||||
|
|||||||
@ -101,4 +101,16 @@ export class EnvironmentService {
|
|||||||
this.configService.get<string>('STORAGE_LOCAL_PATH') ?? '.local-storage'
|
this.configService.get<string>('STORAGE_LOCAL_PATH') ?? '.local-storage'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSupportDriver(): string {
|
||||||
|
return this.configService.get<string>('SUPPORT_DRIVER') ?? 'front';
|
||||||
|
}
|
||||||
|
|
||||||
|
getSupportFrontendKey(): string | null {
|
||||||
|
return this.configService.get<string>('SUPPORT_FRONTEND_KEY') ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSupportHMACKey(): string | null {
|
||||||
|
return this.configService.get<string>('SUPPORT_HMAC_KEY') ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user