Sanitize config props

This commit is contained in:
Ariful Alam
2022-03-27 01:56:05 +06:00
parent 2743dee9aa
commit f75eae4547
5 changed files with 346 additions and 225 deletions

View File

@@ -12,9 +12,13 @@ import Education from './education';
import Project from './project'; import Project from './project';
import Blog from './blog'; import Blog from './blog';
import { import {
constructConfigWithMissingValues, genericError,
getInitialTheme, getInitialTheme,
noConfigError,
notFoundError,
setupHotjar, setupHotjar,
tooManyRequestError,
sanitizeConfig,
} from '../helpers/utils'; } from '../helpers/utils';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
@@ -22,37 +26,31 @@ import '../assets/index.css';
const GitProfile = ({ config }) => { const GitProfile = ({ config }) => {
const [error, setError] = useState( const [error, setError] = useState(
typeof config === 'undefined' typeof config === 'undefined' && !config ? noConfigError : null
? {
status: 500,
title: 'No Config is provided',
subTitle: 'Pass the required config as prop.',
}
: null
); );
const [sanitizedConfig] = useState(
typeof config !== 'undefined' && constructConfigWithMissingValues(config); typeof config === 'undefined' && !config ? null : sanitizeConfig(config)
const [theme, setTheme] = useState(
config ? getInitialTheme(config.themeConfig) : null
); );
const [theme, setTheme] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [profile, setProfile] = useState(null); const [profile, setProfile] = useState(null);
const [repo, setRepo] = useState(null); const [repo, setRepo] = useState(null);
useEffect(() => { useEffect(() => {
if (theme) { if (sanitizedConfig) {
document.documentElement.setAttribute('data-theme', theme); setTheme(getInitialTheme(sanitizedConfig.themeConfig));
setupHotjar(sanitizedConfig.hotjar);
loadData();
} }
}, [theme]); }, [sanitizedConfig]);
useEffect(() => { useEffect(() => {
config && setupHotjar(config.hotjar); theme && document.documentElement.setAttribute('data-theme', theme);
}, []); }, [theme]);
const loadData = useCallback(() => { const loadData = useCallback(() => {
axios axios
.get(`https://api.github.com/users/${config.github.username}`) .get(`https://api.github.com/users/${sanitizedConfig.github.username}`)
.then((response) => { .then((response) => {
let data = response.data; let data = response.data;
@@ -69,14 +67,15 @@ const GitProfile = ({ config }) => {
.then(() => { .then(() => {
let excludeRepo = ``; let excludeRepo = ``;
config.github.exclude.projects.forEach((project) => { sanitizedConfig.github.exclude.projects.forEach((project) => {
excludeRepo += `+-repo:${config.github.username}/${project}`; excludeRepo += `+-repo:${sanitizedConfig.github.username}/${project}`;
}); });
let query = `user:${config.github.username}+fork:${!config.github let query = `user:${
.exclude.forks}${excludeRepo}`; sanitizedConfig.github.username
}+fork:${!sanitizedConfig.github.exclude.forks}${excludeRepo}`;
let url = `https://api.github.com/search/repositories?q=${query}&sort=${config.github.sortBy}&per_page=${config.github.limit}&type=Repositories`; let url = `https://api.github.com/search/repositories?q=${query}&sort=${sanitizedConfig.github.sortBy}&per_page=${sanitizedConfig.github.limit}&type=Repositories`;
axios axios
.get(url, { .get(url, {
@@ -101,10 +100,6 @@ const GitProfile = ({ config }) => {
}); });
}, [setLoading]); }, [setLoading]);
useEffect(() => {
config && loadData();
}, [loadData]);
const handleError = (error) => { const handleError = (error) => {
console.error('Error:', error); console.error('Error:', error);
try { try {
@@ -113,57 +108,25 @@ const GitProfile = ({ config }) => {
).fromNow(); ).fromNow();
if (error.response.status === 403) { if (error.response.status === 403) {
setError({ setError(tooManyRequestError(reset));
status: 429,
title: 'Too Many Requests.',
subTitle: (
<p>
Oh no, you hit the{' '}
<a
href="https://developer.github.com/v3/rate_limit/"
target="_blank"
rel="noopener noreferrer"
>
rate limit.
</a>
! Try again later{` ${reset}`}.
</p>
),
});
} else if (error.response.status === 404) { } else if (error.response.status === 404) {
setError({ setError(notFoundError);
status: 404,
title: 'The Github Username is Incorrect.',
subTitle: (
<p>
Please provide correct github username in <code>config</code>.
</p>
),
});
} else { } else {
setError({ setError(genericError);
status: 500,
title: 'Ops!!',
subTitle: 'Something went wrong.',
});
} }
} catch (error2) { } catch (error2) {
setError({ setError(genericError);
status: 500,
title: 'Ops!!',
subTitle: 'Something went wrong.',
});
} }
}; };
return ( return (
<HelmetProvider> <HelmetProvider>
{!error && ( {sanitizedConfig && (
<HeadTagEditor <HeadTagEditor
profile={profile} profile={profile}
theme={theme} theme={theme}
googleAnalytics={config.googleAnalytics} googleAnalytics={sanitizedConfig.googleAnalytics}
social={config.social} social={sanitizedConfig.social}
/> />
)} )}
<div className="fade-in h-screen"> <div className="fade-in h-screen">
@@ -174,41 +137,39 @@ const GitProfile = ({ config }) => {
subTitle={error.subTitle} subTitle={error.subTitle}
/> />
) : ( ) : (
sanitizedConfig && (
<Fragment> <Fragment>
<div className="p-4 lg:p-10 min-h-full bg-base-200"> <div className="p-4 lg:p-10 min-h-full bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 rounded-box"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 rounded-box">
<div className="col-span-1"> <div className="col-span-1">
<div className="grid grid-cols-1 gap-6"> <div className="grid grid-cols-1 gap-6">
{!error && !config.themeConfig.disableSwitch && ( {!sanitizedConfig.themeConfig.disableSwitch && (
<ThemeChanger <ThemeChanger
theme={theme} theme={theme}
setTheme={setTheme} setTheme={setTheme}
loading={loading} loading={loading}
themeConfig={config.themeConfig} themeConfig={sanitizedConfig.themeConfig}
/> />
)} )}
<AvatarCard profile={profile} loading={loading} /> <AvatarCard profile={profile} loading={loading} />
<Details <Details
profile={profile} profile={profile}
loading={loading} loading={loading}
github={config.github} github={sanitizedConfig.github}
social={config.social} social={sanitizedConfig.social}
/>
<Skill
loading={loading}
skills={sanitizedConfig.skills}
/> />
{typeof config.skills !== 'undefined' && (
<Skill loading={loading} skills={config.skills} />
)}
{typeof config.experiences !== 'undefined' && (
<Experience <Experience
loading={loading} loading={loading}
experiences={config.experiences} experiences={sanitizedConfig.experiences}
/> />
)}
{typeof config.education !== 'undefined' && (
<Education <Education
loading={loading} loading={loading}
education={config.education} education={sanitizedConfig.education}
/> />
)}
</div> </div>
</div> </div>
<div className="lg:col-span-2 col-span-1"> <div className="lg:col-span-2 col-span-1">
@@ -216,16 +177,14 @@ const GitProfile = ({ config }) => {
<Project <Project
repo={repo} repo={repo}
loading={loading} loading={loading}
github={config.github} github={sanitizedConfig.github}
googleAnalytics={config.googleAnalytics} googleAnalytics={sanitizedConfig.googleAnalytics}
/> />
{typeof config.blog !== 'undefined' && (
<Blog <Blog
loading={loading} loading={loading}
googleAnalytics={config.googleAnalytics} googleAnalytics={sanitizedConfig.googleAnalytics}
blog={config.blog} blog={sanitizedConfig.blog}
/> />
)}
</div> </div>
</div> </div>
</div> </div>
@@ -252,6 +211,7 @@ const GitProfile = ({ config }) => {
</div> </div>
</footer> </footer>
</Fragment> </Fragment>
)
)} )}
</div> </div>
</HelmetProvider> </HelmetProvider>
@@ -265,8 +225,8 @@ GitProfile.propTypes = {
sortBy: PropTypes.oneOf(['stars', 'updated']), sortBy: PropTypes.oneOf(['stars', 'updated']),
limit: PropTypes.number, limit: PropTypes.number,
exclude: PropTypes.shape({ exclude: PropTypes.shape({
forks: PropTypes.bool.isRequired, forks: PropTypes.bool,
projects: PropTypes.array.isRequired, projects: PropTypes.array,
}), }),
}).isRequired, }).isRequired,
social: PropTypes.shape({ social: PropTypes.shape({
@@ -311,11 +271,11 @@ GitProfile.propTypes = {
snippetVersion: PropTypes.number, snippetVersion: PropTypes.number,
}), }),
themeConfig: PropTypes.shape({ themeConfig: PropTypes.shape({
defaultTheme: PropTypes.string.isRequired, defaultTheme: PropTypes.string,
disableSwitch: PropTypes.bool.isRequired, disableSwitch: PropTypes.bool,
respectPrefersColorScheme: PropTypes.bool.isRequired, respectPrefersColorScheme: PropTypes.bool,
themes: PropTypes.array.isRequired, themes: PropTypes.array,
customTheme: PropTypes.object.isRequired, customTheme: PropTypes.object,
}), }),
}).isRequired, }).isRequired,
}; };

View File

@@ -57,7 +57,7 @@ const HeadTagEditor = ({ profile, theme, googleAnalytics, social }) => {
HeadTagEditor.propTypes = { HeadTagEditor.propTypes = {
profile: PropTypes.object, profile: PropTypes.object,
theme: PropTypes.string.isRequired, theme: PropTypes.string,
googleAnalytics: PropTypes.object.isRequired, googleAnalytics: PropTypes.object.isRequired,
social: PropTypes.object.isRequired, social: PropTypes.object.isRequired,
}; };

View File

@@ -89,7 +89,7 @@ const ThemeChanger = ({ theme, setTheme, loading, themeConfig }) => {
}; };
ThemeChanger.propTypes = { ThemeChanger.propTypes = {
theme: PropTypes.string.isRequired, theme: PropTypes.string,
setTheme: PropTypes.func.isRequired, setTheme: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired, loading: PropTypes.bool.isRequired,
themeConfig: PropTypes.object.isRequired, themeConfig: PropTypes.object.isRequired,

View File

@@ -98,25 +98,26 @@ export const setupHotjar = (hotjarConfig) => {
} }
}; };
export const constructConfigWithMissingValues = (config) => { export const validateConfig = (config) => {
if (typeof config.github.sortBy === 'undefined') { const customTheme =
Object.assign(config.github, { sortBy: 'stars' }); typeof config.themeConfig !== 'undefined' &&
} typeof config.themeConfig.customTheme !== 'undefined'
? config.themeConfig.customTheme
: {
primary: '#fc055b',
secondary: '#219aaf',
accent: '#e8d03a',
neutral: '#2A2730',
'base-100': '#E3E3ED',
'--rounded-box': '3rem',
'--rounded-btn': '3rem',
};
if (typeof config.github.limit === 'undefined') { const themes =
Object.assign(config.github, { limit: 6 }); typeof config.themeConfig !== 'undefined' &&
} typeof config.themeConfig.themes !== 'undefined'
? config.themeConfig.themes
if (typeof config.github.exclude === 'undefined') { : [
Object.assign(config.github, { exclude: { forks: false, projects: [] } });
}
if (typeof config.themeConfig === 'undefined') {
const themeConfig = {
defaultTheme: 'corporate',
disableSwitch: false,
respectPrefersColorScheme: false,
themes: [
'light', 'light',
'dark', 'dark',
'cupcake', 'cupcake',
@@ -147,36 +148,182 @@ export const constructConfigWithMissingValues = (config) => {
'coffee', 'coffee',
'winter', 'winter',
'procyon', 'procyon',
], ];
customTheme: {
procyon: { return {
primary: '#fc055b', github: {
secondary: '#219aaf', username: config.github.username,
accent: '#e8d03a', sortBy:
neutral: '#2A2730', typeof config.github.sortBy !== 'undefined'
'base-100': '#E3E3ED', ? config.github.sortBy
'--rounded-box': '3rem', : 'stars',
'--rounded-btn': '3rem', limit:
typeof config.github.limit !== 'undefined' ? config.github.limit : 8,
exclude: {
forks:
typeof config.github.exclude !== 'undefined' &&
typeof config.github.exclude.forks !== 'undefined'
? config.github.exclude.forks
: false,
projects:
typeof config.github.exclude !== 'undefined' &&
typeof config.github.exclude.projects !== 'undefined'
? config.github.exclude.projects
: [],
}, },
}, },
social: {
linkedin:
typeof config.social !== 'undefined' &&
typeof config.social.linkedin !== 'undefined'
? config.social.linkedin
: '',
twitter:
typeof config.social !== 'undefined' &&
typeof config.social.twitter !== 'undefined'
? config.social.twitter
: '',
facebook:
typeof config.social !== 'undefined' &&
typeof config.social.facebook !== 'undefined'
? config.social.facebook
: '',
dribbble:
typeof config.social !== 'undefined' &&
typeof config.social.dribbble !== 'undefined'
? config.social.dribbble
: '',
behance:
typeof config.social !== 'undefined' &&
typeof config.social.behance !== 'undefined'
? config.social.behance
: '',
medium:
typeof config.social !== 'undefined' &&
typeof config.social.medium !== 'undefined'
? config.social.medium
: '',
devto:
typeof config.social !== 'undefined' &&
typeof config.social.devto !== 'undefined'
? config.social.devto
: '',
website:
typeof config.social !== 'undefined' &&
typeof config.social.website !== 'undefined'
? config.social.website
: '',
phone:
typeof config.social !== 'undefined' &&
typeof config.social.phone !== 'undefined'
? config.social.phone
: '',
email:
typeof config.social !== 'undefined' &&
typeof config.social.email !== 'undefined'
? config.social.email
: '',
},
skills: typeof config.skills !== 'undefined' ? config.skills : [],
experiences:
typeof config.experiences !== 'undefined' ? config.experiences : [],
education: typeof config.education !== 'undefined' ? config.education : [],
blog: {
source:
typeof config.blog !== 'undefined' &&
typeof config.blog.source !== 'undefined'
? config.blog.source
: '',
username:
typeof config.blog !== 'undefined' &&
typeof config.blog.username !== 'undefined'
? config.blog.username
: '',
limit:
typeof config.blog !== 'undefined' &&
typeof config.blog.limit !== 'undefined'
? config.blog.limit
: 10,
},
googleAnalytics: {
id:
typeof config.googleAnalytics !== 'undefined' &&
typeof config.googleAnalytics.id !== 'undefined'
? config.googleAnalytics.id
: '',
},
hotjar: {
id:
typeof config.hotjar !== 'undefined' &&
typeof config.hotjar.id !== 'undefined'
? config.hotjar.id
: '',
snippetVersion:
typeof config.hotjar !== 'undefined' &&
typeof config.hotjar.snippetVersion !== 'undefined'
? config.hotjar.snippetVersion
: 6,
},
themeConfig: {
defaultTheme:
typeof config.themeConfig !== 'undefined' &&
typeof config.themeConfig.defaultTheme !== 'undefined'
? config.themeConfig.defaultTheme
: themes[0],
disableSwitch:
typeof config.themeConfig !== 'undefined' &&
typeof config.themeConfig.disableSwitch !== 'undefined'
? config.themeConfig.disableSwitch
: false,
respectPrefersColorScheme:
typeof config.themeConfig !== 'undefined' &&
typeof config.themeConfig.respectPrefersColorScheme !== 'undefined'
? config.themeConfig.respectPrefersColorScheme
: false,
themes: themes,
customTheme: customTheme,
},
};
}; };
Object.assign(config, { themeConfig: themeConfig }); export const noConfigError = {
} status: 500,
title: 'No Config is provided.',
if (typeof config.googleAnalytics === 'undefined') { subTitle: 'Pass the required config as prop.',
const googleAnalytics = {
id: '',
}; };
Object.assign(config, { googleAnalytics: googleAnalytics }); export const tooManyRequestError = (reset) => {
} return {
status: 429,
if (typeof config.social === 'undefined') { title: 'Too Many Requests.',
const social = {}; subTitle: (
<p>
Object.assign(config, { social: social }); Oh no, you hit the{' '}
} <a
href="https://developer.github.com/v3/rate_limit/"
return config; target="_blank"
rel="noopener noreferrer"
>
rate limit.
</a>
! Try again later{` ${reset}`}.
</p>
),
};
};
export const notFoundError = {
status: 404,
title: 'The Github Username is Incorrect.',
subTitle: (
<p>
Please provide correct github username in <code>config</code>.
</p>
),
};
export const genericError = {
status: 500,
title: 'Ops!!',
subTitle: 'Something went wrong.',
}; };

44
types/index.d.ts vendored
View File

@@ -27,14 +27,14 @@ export interface Github {
/** /**
* Forked projects will not be displayed if set to true * Forked projects will not be displayed if set to true
*/ */
forks: boolean; forks?: boolean;
/** /**
* These projects will not be displayed * These projects will not be displayed
* *
* example: ['my-project1', 'my-project2'] * example: ['my-project1', 'my-project2']
*/ */
projects: Array<string>; projects?: Array<string>;
}; };
} }
@@ -94,65 +94,79 @@ export interface Blog {
/** /**
* medium | dev.to * medium | dev.to
*/ */
source: string; source?: string;
/** /**
* Username * Username
*/ */
username: string; username?: string;
/** /**
* How many posts to display * How many posts to display
* *
* Max is 10 * Max is 10
*/ */
limit: number; limit?: number;
} }
export interface GoogleAnalytics { export interface GoogleAnalytics {
/** /**
* GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX * GA3 tracking id/GA4 tag id UA-XXXXXXXXX-X | G-XXXXXXXXXX
*/ */
id: string; id?: string;
} }
export interface Hotjar { export interface Hotjar {
/** /**
* Hotjar id * Hotjar id
*/ */
id: string; id?: string;
/** /**
* Snippet Version * Snippet Version
*/ */
snippetVersion: number; snippetVersion?: number;
} }
export interface ThemeConfig { export interface ThemeConfig {
/** /**
* Default theme * Default theme
*/ */
defaultTheme: string; defaultTheme?: string;
/** /**
* Hides the switch in the navbar * Hides the switch in the navbar
*/ */
disableSwitch: boolean; disableSwitch?: boolean;
/** /**
* Should use the prefers-color-scheme media-query * Should use the prefers-color-scheme media-query
*/ */
respectPrefersColorScheme: boolean; respectPrefersColorScheme?: boolean;
/** /**
* Available themes * Available themes
*/ */
themes: Array<string>; themes?: Array<string>;
/** /**
* Custom theme * Custom theme
*/ */
customTheme: object; customTheme?: object;
}
export interface Experience {
company?: string;
position?: string;
from?: string;
to?: string;
}
export interface Education {
institution?: string;
degree?: string;
from?: string;
to?: string;
} }
export interface Config { export interface Config {
@@ -174,12 +188,12 @@ export interface Config {
/** /**
* Experience list * Experience list
*/ */
experiences?: Array<string>; experiences?: Array<Experience>;
/** /**
* Education list * Education list
*/ */
education?: Array<string>; education?: Array<Education>;
/** /**
* Blog config * Blog config