feat: implement user impersonation feature (#976)
* feat: wip impersonate user * feat: add ability to impersonate an user * fix: remove console.log * fix: unused import
This commit is contained in:
@ -39,8 +39,10 @@ export const VERIFY = gql`
|
||||
displayName
|
||||
firstName
|
||||
lastName
|
||||
canImpersonate
|
||||
workspaceMember {
|
||||
id
|
||||
allowImpersonation
|
||||
workspace {
|
||||
id
|
||||
domainName
|
||||
@ -85,3 +87,45 @@ export const RENEW_TOKEN = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// TODO: Fragments should be used instead of duplicating the user fields !
|
||||
export const IMPERSONATE = gql`
|
||||
mutation Impersonate($userId: String!) {
|
||||
impersonate(userId: $userId) {
|
||||
user {
|
||||
id
|
||||
email
|
||||
displayName
|
||||
firstName
|
||||
lastName
|
||||
canImpersonate
|
||||
workspaceMember {
|
||||
id
|
||||
allowImpersonation
|
||||
workspace {
|
||||
id
|
||||
domainName
|
||||
displayName
|
||||
logo
|
||||
inviteHash
|
||||
}
|
||||
}
|
||||
settings {
|
||||
id
|
||||
colorScheme
|
||||
locale
|
||||
}
|
||||
}
|
||||
tokens {
|
||||
accessToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
refreshToken {
|
||||
token
|
||||
expiresAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { Toggle } from '@/ui/input/toggle/components/Toggle';
|
||||
import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
|
||||
import { useUpdateAllowImpersonationMutation } from '~/generated/graphql';
|
||||
|
||||
export function ToggleField() {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const [updateAllowImpersonation] = useUpdateAllowImpersonationMutation();
|
||||
|
||||
async function handleChange(value: boolean) {
|
||||
try {
|
||||
const { data, errors } = await updateAllowImpersonation({
|
||||
variables: {
|
||||
allowImpersonation: value,
|
||||
},
|
||||
});
|
||||
|
||||
if (errors || !data?.allowImpersonation) {
|
||||
throw new Error('Error while updating user');
|
||||
}
|
||||
} catch (err: any) {
|
||||
enqueueSnackBar(err?.message, {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
value={currentUser?.workspaceMember?.allowImpersonation}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -17,4 +17,7 @@ export enum AppPath {
|
||||
PersonShowPage = '/person/:personId',
|
||||
OpportunitiesPage = '/opportunities',
|
||||
SettingsCatchAll = `/settings/*`,
|
||||
|
||||
// Impersonate
|
||||
Impersonate = '/impersonate/:userId',
|
||||
}
|
||||
|
||||
63
front/src/modules/ui/input/toggle/components/Toggle.tsx
Normal file
63
front/src/modules/ui/input/toggle/components/Toggle.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
type ContainerProps = {
|
||||
isOn: boolean;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
const Container = styled.div<ContainerProps>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme, isOn, color }) =>
|
||||
isOn ? color ?? theme.color.blue : theme.background.quaternary};
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
transition: background-color 0.3s ease;
|
||||
width: 32px;
|
||||
`;
|
||||
|
||||
const Circle = styled(motion.div)`
|
||||
background-color: #fff;
|
||||
border-radius: 50%;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
const circleVariants = {
|
||||
on: { x: 14 },
|
||||
off: { x: 2 },
|
||||
};
|
||||
|
||||
export type ToggleProps = {
|
||||
value?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export function Toggle({ value, onChange, color }: ToggleProps) {
|
||||
const [isOn, setIsOn] = useState(value ?? false);
|
||||
|
||||
function handleChange() {
|
||||
setIsOn(!isOn);
|
||||
|
||||
if (onChange) {
|
||||
onChange(!isOn);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== isOn) {
|
||||
setIsOn(value ?? false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Container onClick={handleChange} isOn={isOn} color={color}>
|
||||
<Circle animate={isOn ? 'on' : 'off'} variants={circleVariants} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@ import styled from '@emotion/styled';
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
addornment?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -11,6 +12,12 @@ const StyledContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
@ -26,10 +33,13 @@ const StyledDescription = styled.h3`
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
export function H2Title({ title, description }: Props) {
|
||||
export function H2Title({ title, description, addornment }: Props) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
<StyledTitleContainer>
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{addornment}
|
||||
</StyledTitleContainer>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -10,8 +10,10 @@ export const GET_CURRENT_USER = gql`
|
||||
firstName
|
||||
lastName
|
||||
avatarUrl
|
||||
canImpersonate
|
||||
workspaceMember {
|
||||
id
|
||||
allowImpersonation
|
||||
workspace {
|
||||
id
|
||||
domainName
|
||||
|
||||
@ -28,6 +28,15 @@ export const UPDATE_USER = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_ALLOW_IMPERONATION = gql`
|
||||
mutation UpdateAllowImpersonation($allowImpersonation: Boolean!) {
|
||||
allowImpersonation(allowImpersonation: $allowImpersonation) {
|
||||
id
|
||||
allowImpersonation
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_PROFILE_PICTURE = gql`
|
||||
mutation UploadProfilePicture($file: Upload!) {
|
||||
uploadProfilePicture(file: $file)
|
||||
|
||||
Reference in New Issue
Block a user