Skip to content

Commit

Permalink
feat: Integrated Dicebear Library To Enhance Security (#1585)
Browse files Browse the repository at this point in the history
* feat: Integrated Dicebear Library To Enhance Security

- Added the Dicebear library as a dependency to the project.
- Imported the necessary functions from the library.
- Created a new `Avatar` component that utilizes the Dicebear library locally.
- Updated the component to accept `name`, `alt`, and `size` as props.
- Utilized the Dicebear library to generate avatars with initials.
- Removed direct HTTP API calls from the codebase.
- Replaced instances of direct HTTP API calls with the newly created `Avatar` component.
- Updated the exisiting tests and ensured the tests are still valid.
- Mitigated security risks associated with SSRF and XSS attacks by eliminating direct HTTP API calls.
- By using the Dicebear library locally, we reduce potential vulnerabilities.
- Ensured no other functionality is affected.

Signed-off-by: Akhilender <[email protected]>

* fix: Reverted changes in the tests

- Reverted the Avatar component in all the tests as they are redundant in tests except where it is needed.
- Ensured all the remaning tests are succesfully passing.

Signed-off-by: Akhilender <[email protected]>

* fix: Added dynamic style

- Handled case where no alt tag is provided by adding default alt value.
- dynamic rendering for className for img tag in avatar component
- Dynamic rendering for test-id
- Replaced suitable changes for the above properties added

Signed-off-by: Akhilender <[email protected]>

* fix: modified test

Signed-off-by: Akhilender <[email protected]>

* fix: resolved conflict

Signed-off-by: Akhilender <[email protected]>

---------

Signed-off-by: Akhilender <[email protected]>
  • Loading branch information
akhilender-bongirwar authored Feb 28, 2024
1 parent 092b9f6 commit 7828c40
Show file tree
Hide file tree
Showing 14 changed files with 1,291 additions and 580 deletions.
1 change: 1 addition & 0 deletions __mocks__/@dicebear/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const initials = jest.fn();
5 changes: 5 additions & 0 deletions __mocks__/@dicebear/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const createAvatar = jest.fn(() => {
return {
toDataUriSync: jest.fn(() => 'mocked-data-uri'),
};
});
2 changes: 2 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default {
moduleNameMapper: {
'^react-native$': 'react-native-web',
'^@mui/(.*)$': '<rootDir>/node_modules/@mui/$1',
'^@dicebear/core$': '<rootDir>/__mocks__/@dicebear/core.ts',
'^@dicebear/collection$': '<rootDir>/__mocks__/@dicebear/collection.ts',
},
moduleFileExtensions: [
'web.js',
Expand Down
1,634 changes: 1,120 additions & 514 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"@apollo/client": "^3.4.0-beta.19",
"@apollo/link-error": "^2.0.0-beta.3",
"@apollo/react-testing": "^4.0.0",
"@dicebear/collection": "^7.0.4",
"@dicebear/core": "^7.0.4",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.8.3",
Expand Down
50 changes: 50 additions & 0 deletions src/components/Avatar/Avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React from 'react';
import { render } from '@testing-library/react';
import Avatar from './Avatar';
import { BrowserRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import i18nForTest from 'utils/i18nForTest';

describe('Avatar component', () => {
test('renders with name and alt attribute', () => {
const testName = 'John Doe';
const testAlt = 'Test Alt Text';
const testSize = 64;

const { getByAltText } = render(
<BrowserRouter>
<I18nextProvider i18n={i18nForTest}>
<Avatar name={testName} alt={testAlt} size={testSize} />
</I18nextProvider>
</BrowserRouter>,
);
const avatarElement = getByAltText(testAlt);

expect(avatarElement).toBeInTheDocument();
expect(avatarElement.getAttribute('src')).toBeDefined();
});

test('renders with custom style and data-testid', () => {
const testName = 'Jane Doe';
const testStyle = 'custom-avatar-style';
const testDataTestId = 'custom-avatar-test-id';

const { getByAltText } = render(
<BrowserRouter>
<I18nextProvider i18n={i18nForTest}>
<Avatar
name={testName}
avatarStyle={testStyle}
dataTestId={testDataTestId}
/>
</I18nextProvider>
</BrowserRouter>,
);
const avatarElement = getByAltText('Dummy Avatar');

expect(avatarElement).toBeInTheDocument();
expect(avatarElement.getAttribute('src')).toBeDefined();
expect(avatarElement.getAttribute('class')).toContain(testStyle);
expect(avatarElement.getAttribute('data-testid')).toBe(testDataTestId);
});
});
39 changes: 39 additions & 0 deletions src/components/Avatar/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useMemo } from 'react';
import { createAvatar } from '@dicebear/core';
import { initials } from '@dicebear/collection';

interface InterfaceAvatarProps {
name: string;
alt?: string;
size?: number;
avatarStyle?: string;
dataTestId?: string;
}

const Avatar = ({
name,
alt = 'Dummy Avatar',
size,
avatarStyle,
dataTestId,
}: InterfaceAvatarProps): JSX.Element => {
const avatar = useMemo(() => {
return createAvatar(initials, {
size: size || 128,
seed: name,
}).toDataUriSync();
}, [name, size]);

const svg = avatar.toString();

return (
<img
src={svg}
alt={alt}
className={avatarStyle ? avatarStyle : ''}
data-testid={dataTestId ? dataTestId : ''}
/>
);
};

export default Avatar;
5 changes: 3 additions & 2 deletions src/components/LeftDrawer/LeftDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import styles from './LeftDrawer.module.css';
import { useMutation } from '@apollo/client';
import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
import useLocalStorage from 'utils/useLocalstorage';
import Avatar from 'components/Avatar/Avatar';

export interface InterfaceLeftDrawerProps {
hideDrawer: boolean | null;
Expand Down Expand Up @@ -115,8 +116,8 @@ const leftDrawer = ({
{userImage && userImage !== 'null' ? (
<img src={userImage} alt={`profile picture`} />
) : (
<img
src={`https://api.dicebear.com/5.x/initials/svg?seed=${firstName}%20${lastName}`}
<Avatar
name={`${firstName} ${lastName}`}
alt={`dummy picture`}
/>
)}
Expand Down
11 changes: 5 additions & 6 deletions src/components/LeftDrawerEvent/LeftDrawerEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { EventStatsWrapper } from 'components/EventStats/EventStatsWrapper';
import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
import { useMutation } from '@apollo/client';
import useLocalStorage from 'utils/useLocalstorage';
import Avatar from 'components/Avatar/Avatar';

export interface InterfaceLeftDrawerProps {
event: {
Expand Down Expand Up @@ -68,10 +69,8 @@ const leftDrawerEvent = ({
<div className={styles.organizationContainer}>
<button className={styles.profileContainer} data-testid="OrgBtn">
<div className={styles.imageContainer}>
<img
src={`https://api.dicebear.com/5.x/initials/svg?seed=${event.title
.split(' ')
.join('%20')}`}
<Avatar
name={event.title.split(' ').join('%20')}
alt="Dummy Event Picture"
/>
</div>
Expand Down Expand Up @@ -135,8 +134,8 @@ const leftDrawerEvent = ({
{userImage && userImage !== 'null' ? (
<img src={userImage} alt={`Profile Picture`} />
) : (
<img
src={`https://api.dicebear.com/5.x/initials/svg?seed=${firstName}%20${lastName}`}
<Avatar
name={`${firstName} ${lastName}`}
alt={`Dummy User Picture`}
/>
)}
Expand Down
11 changes: 6 additions & 5 deletions src/components/LeftDrawerOrg/LeftDrawerOrg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ReactComponent as TalawaLogo } from 'assets/svgs/talawa.svg';
import styles from './LeftDrawerOrg.module.css';
import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
import useLocalStorage from 'utils/useLocalstorage';
import Avatar from 'components/Avatar/Avatar';

export interface InterfaceLeftDrawerProps {
orgId: string;
Expand Down Expand Up @@ -118,9 +119,9 @@ const leftDrawerOrg = ({
{organization.image ? (
<img src={organization.image} alt={`profile picture`} />
) : (
<img
src={`https://api.dicebear.com/5.x/initials/svg?seed=${organization.name}`}
alt={`Dummy Organization Picture`}
<Avatar
name={organization.name}
alt={'Dummy Organization Picture'}
/>
)}
</div>
Expand Down Expand Up @@ -187,8 +188,8 @@ const leftDrawerOrg = ({
{userImage && userImage !== 'null' ? (
<img src={userImage} alt={`profile picture`} />
) : (
<img
src={`https://api.dicebear.com/5.x/initials/svg?seed=${firstName}%20${lastName}`}
<Avatar
name={`${firstName} ${lastName}`}
alt={`dummy picture`}
/>
)}
Expand Down
23 changes: 10 additions & 13 deletions src/components/OrgListCard/OrgListCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LocationOnIcon from '@mui/icons-material/LocationOn';
import { IS_SAMPLE_ORGANIZATION_QUERY } from 'GraphQl/Queries/Queries';
import { useQuery } from '@apollo/client';
import { Tooltip } from '@mui/material';
import Avatar from 'components/Avatar/Avatar';

export interface InterfaceOrgListCardProps {
data: InterfaceOrgConnectionInfoType;
Expand Down Expand Up @@ -42,19 +43,15 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element {
<div className={styles.orgCard}>
<div className={styles.innerContainer}>
<div className={styles.orgImgContainer}>
<img
src={
image
? image
: `https://api.dicebear.com/5.x/initials/svg?seed=${name
.split(/\s+/)
.map((word) => word.charAt(0))
.slice(0, 2)
.join('')}`
}
alt={`${name} image`}
data-testid={image ? '' : 'emptyContainerForImage'}
/>
{image ? (
<img src={image} alt={`${name} image`} />
) : (
<Avatar
name={name}
alt={`${name} image`}
dataTestId="emptyContainerForImage"
/>
)}
</div>
<div className={styles.content}>
<Tooltip title={name} placement="top-end">
Expand Down
18 changes: 14 additions & 4 deletions src/components/UserPortal/ContactCard/ContactCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import styles from './ContactCard.module.css';
import Avatar from 'components/Avatar/Avatar';

interface InterfaceContactCardProps {
id: string;
Expand All @@ -14,9 +15,6 @@ interface InterfaceContactCardProps {

function contactCard(props: InterfaceContactCardProps): JSX.Element {
const contactName = `${props.firstName} ${props.lastName}`;
const imageUrl = props.image
? props.image
: `https://api.dicebear.com/5.x/initials/svg?seed=${contactName}`;

const handleSelectedContactChange = (): void => {
props.setSelectedContact(props.id);
Expand All @@ -40,7 +38,19 @@ function contactCard(props: InterfaceContactCardProps): JSX.Element {
onClick={handleSelectedContactChange}
data-testid="contactContainer"
>
<img src={imageUrl} alt={contactName} className={styles.contactImage} />
{props.image ? (
<img
src={props.image}
alt={contactName}
className={styles.contactImage}
/>
) : (
<Avatar
name={contactName}
alt={contactName}
avatarStyle={styles.contactImage}
/>
)}
<div className={styles.contactNameContainer}>
<b>{contactName}</b>
<small className={styles.grey}>{props.email}</small>
Expand Down
59 changes: 27 additions & 32 deletions src/components/UsersTableItem/UsersTableItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { toast } from 'react-toastify';
import { errorHandler } from 'utils/errorHandler';
import type { InterfaceQueryUserListItem } from 'utils/interfaces';
import styles from './UsersTableItem.module.css';
import Avatar from 'components/Avatar/Avatar';

type Props = {
user: InterfaceQueryUserListItem;
Expand Down Expand Up @@ -306,14 +307,11 @@ const UsersTableItem = (props: Props): JSX.Element => {
className="p-0"
onClick={() => goToOrg(org._id)}
>
<img
src={
org.image
? org.image
: `https://api.dicebear.com/5.x/initials/svg?seed=${org.name}`
}
alt="orgImage"
/>
{org.image ? (
<img src={org.image} alt="orgImage" />
) : (
<Avatar name={org.name} alt="orgImage" />
)}
{org.name}
</Button>
</td>
Expand All @@ -326,14 +324,14 @@ const UsersTableItem = (props: Props): JSX.Element => {
onClick={() => handleCreator()}
data-testid={`creator${org._id}`}
>
<img
src={
org.creator.image
? org.creator.image
: `https://api.dicebear.com/5.x/initials/svg?seed=${org.creator.firstName} ${org.creator.lastName}`
}
alt="creator"
/>
{org.creator.image ? (
<img src={org.creator.image} alt="creator" />
) : (
<Avatar
name={`${org.creator.firstName} ${org.creator.lastName}`}
alt="creator"
/>
)}
{org.creator.firstName} {org.creator.lastName}
</Button>
</td>
Expand Down Expand Up @@ -483,14 +481,11 @@ const UsersTableItem = (props: Props): JSX.Element => {
className="p-0"
onClick={() => goToOrg(org._id)}
>
<img
src={
org.image
? org.image
: `https://api.dicebear.com/5.x/initials/svg?seed=${org.name}`
}
alt="orgImage"
/>
{org.image ? (
<img src={org.image} alt="orgImage" />
) : (
<Avatar name={org.name} alt="orgImage" />
)}
{org.name}
</Button>
</td>
Expand All @@ -503,14 +498,14 @@ const UsersTableItem = (props: Props): JSX.Element => {
onClick={() => handleCreator()}
data-testid={`creator${org._id}`}
>
<img
src={
org.creator.image
? org.creator.image
: `https://api.dicebear.com/5.x/initials/svg?seed=${org.creator.firstName} ${org.creator.lastName}`
}
alt="creator"
/>
{org.creator.image ? (
<img src={org.creator.image} alt="creator" />
) : (
<Avatar
name={`${org.creator.firstName} ${org.creator.lastName}`}
alt="creator"
/>
)}
{org.creator.firstName} {org.creator.lastName}
</Button>
</td>
Expand Down
11 changes: 7 additions & 4 deletions src/screens/MemberDetail/MemberDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { toast } from 'react-toastify';
import { errorHandler } from 'utils/errorHandler';
import Loader from 'components/Loader/Loader';
import useLocalStorage from 'utils/useLocalstorage';
import Avatar from 'components/Avatar/Avatar';

const { getItem } = useLocalStorage();

Expand Down Expand Up @@ -154,10 +155,12 @@ const MemberDetail: React.FC<MemberDetailProps> = ({ id }): JSX.Element => {
data-testid="userImagePresent"
/>
) : (
<img
className={styles.userImage}
src={`https://api.dicebear.com/5.x/initials/svg?seed=${userData?.user?.firstName} ${userData?.user?.lastName}`}
data-testid="userImageAbsent"
<Avatar
name={`${userData?.user?.firstName} ${userData?.user?.lastName}`}
alt="User Image"
size={180}
avatarStyle={styles.userImage}
dataTestId="userImageAbsent"
/>
)}
</div>
Expand Down

0 comments on commit 7828c40

Please sign in to comment.