diff --git a/package.json b/package.json index 62ff518..8150e6c 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,12 @@ }, "dependencies": { "@sentry/nextjs": "^7.86.0", + "@types/bcryptjs": "^2.4.6", "@vercel/og": "^0.5.20", "archiver": "^6.0.1", "aws-sdk": "^2.1515.0", "axios": "^1.6.2", + "bcryptjs": "^2.4.3", "chalk": "^5.3.0", "date-fns": "^2.30.0", "file-saver": "^2.0.5", diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx index be9f259..05b4902 100644 --- a/src/components/ShareButton.tsx +++ b/src/components/ShareButton.tsx @@ -17,19 +17,18 @@ const defaultText = 'Check out my GitHub Unwrapped of 2023!'; export const ShareButton: React.FC = ({ callBack, - userName, - imageName, + imageUrl, className }) => { const [isMenuOpen, setIsMenuOpen] = useState(false); const [tweetText, setTweetText] = useState(''); const domain = window.location.origin; - const imageUrl = `${domain}/shared/${userName}/${imageName}`; + const completeUrl = `${domain}${imageUrl}`; const shareToTwitter = () => { const encodedText = encodeURIComponent(tweetText || defaultText); - const encodedImageUrl = encodeURIComponent(imageUrl); + const encodedImageUrl = encodeURIComponent(completeUrl); const twitterShareUrl = `https://twitter.com/intent/tweet?text=${encodedText}&url=${encodedImageUrl}`; @@ -55,7 +54,7 @@ export const ShareButton: React.FC = ({ className="cursor-pointer" onClick={toggleMenu} /> - + {isMenuOpen && (
diff --git a/src/components/SwiperCarousel.tsx b/src/components/SwiperCarousel.tsx index 3411db7..e6ffef1 100644 --- a/src/components/SwiperCarousel.tsx +++ b/src/components/SwiperCarousel.tsx @@ -14,6 +14,7 @@ import { ShareButton } from './ShareButton'; import { UpdatedImageFile } from '@/types/images'; import { CardTypes, sequence } from '@/types/cards'; import { track } from '@/constants/events'; +import { extractFilenameWithoutExtension } from '@/utils/stringHelpers'; interface SwiperCarouselProps { images: UpdatedImageFile[]; @@ -92,6 +93,7 @@ const SwiperCarousel: React.FC = ({ { singleImageSharingCallback({ images: image }); @@ -135,11 +137,3 @@ const sortImageCards = (imageA: UpdatedImageFile, imageB: UpdatedImageFile) => { return indexOfA - indexOfB; }; - -const extractFilenameWithoutExtension = (input: string): string => { - const parts = input.split('/'); - const filenameWithExtension = parts[parts.length - 1]; - const filenameParts = filenameWithExtension.split('.'); - const filenameWithoutExtension = filenameParts[0]; - return filenameWithoutExtension || ''; -}; diff --git a/src/constants/general.ts b/src/constants/general.ts index 6dc9df1..5fd1877 100644 --- a/src/constants/general.ts +++ b/src/constants/general.ts @@ -56,3 +56,5 @@ export const cardColorsMap = { green: '#71CB7F', midnight: '#442773' }; + +export const HASH_LENGTH = 6; diff --git a/src/pages/api/download.ts b/src/pages/api/download.ts index 5870fd4..4be82fe 100644 --- a/src/pages/api/download.ts +++ b/src/pages/api/download.ts @@ -8,6 +8,10 @@ import { dec } from '@/api-helpers/auth-supplementary'; import { fetchSavedCards, saveCards } from '@/api-helpers/persistance'; import { fetchUser } from '@/api-helpers/exapi-sdk/github'; import { GithubUser } from '@/api-helpers/exapi-sdk/types'; +import { + bcryptGen, + extractFilenameWithoutExtension +} from '@/utils/stringHelpers'; const fetchAndDownloadImageBuffer = async ( req: NextApiRequest, @@ -60,10 +64,17 @@ const fetchAndDownloadImageBuffer = async ( } else { res.setHeader('Content-Type', 'application/json'); res.send( - imageBuffer.map(({ data, fileName }) => ({ - fileName, - data: `data:image/png;base64,${data.toString('base64')}` - })) + imageBuffer.map(({ data, fileName }) => { + const file = extractFilenameWithoutExtension(fileName); + const username = user.login; + const hash = bcryptGen(username + file); + + return { + fileName, + url: `/shared/${username}/${file}/${hash}`, + data: `data:image/png;base64,${data.toString('base64')}` + }; + }) ); } console.log(chalk.green('Successfully sent buffer to client')); diff --git a/src/pages/api/shared/[username]/[cardname].ts b/src/pages/api/shared/[username]/[cardname]/[...hash].ts similarity index 78% rename from src/pages/api/shared/[username]/[cardname].ts rename to src/pages/api/shared/[username]/[cardname]/[...hash].ts index 62b2081..2da2931 100644 --- a/src/pages/api/shared/[username]/[cardname].ts +++ b/src/pages/api/shared/[username]/[cardname]/[...hash].ts @@ -1,6 +1,7 @@ import chalk from 'chalk'; import { NextApiRequest, NextApiResponse } from 'next'; import { fetchSavedCard } from '@/api-helpers/persistance'; +import { bcryptGen } from '@/utils/stringHelpers'; const fetchAndDownloadImageBuffer = async ( req: NextApiRequest, @@ -9,6 +10,15 @@ const fetchAndDownloadImageBuffer = async ( let username = req.query.username as string; let cardName = req.query.cardname as string; + const generatedHash = bcryptGen(username + cardName); + const linkHash = (req.query.hash as string[]).join('/') as string; + + if (generatedHash !== linkHash) { + return res.status(400).json({ + message: 'Invalid parameters, must pass valid hash.' + }); + } + if (!username) { return res.status(400).json({ message: 'Invalid parameters, must pass valid username.' diff --git a/src/pages/api/shared/[username]/index.ts b/src/pages/api/shared/[username]/[cardname]/index.ts similarity index 100% rename from src/pages/api/shared/[username]/index.ts rename to src/pages/api/shared/[username]/[cardname]/index.ts diff --git a/src/types/images.ts b/src/types/images.ts index 4748868..47696c3 100644 --- a/src/types/images.ts +++ b/src/types/images.ts @@ -9,4 +9,5 @@ export type ImageFile = { export type UpdatedImageFile = { fileName: string; data: string; + url: string; }; diff --git a/src/utils/stringHelpers.ts b/src/utils/stringHelpers.ts index cd95f2b..2d34b91 100644 --- a/src/utils/stringHelpers.ts +++ b/src/utils/stringHelpers.ts @@ -1,4 +1,22 @@ +import { hashSync } from 'bcryptjs'; +import { HASH_LENGTH } from '@/constants/general'; + export const capitalize = (sentence?: string): string => { if (!sentence) return ''; return sentence.replace(/\b\w/g, (match) => match.toUpperCase()); }; + +export const bcryptGen = (username: string): string => { + const salt = process.env.HASHING_SALT as string; + if (!salt) return ''; + const hash = hashSync(username, salt); + return hash.slice(-HASH_LENGTH); +}; + +export const extractFilenameWithoutExtension = (input: string): string => { + const parts = input.split('/'); + const filenameWithExtension = parts[parts.length - 1]; + const filenameParts = filenameWithExtension.split('.'); + const filenameWithoutExtension = filenameParts[0]; + return filenameWithoutExtension || ''; +}; diff --git a/yarn.lock b/yarn.lock index bd0f006..d83a5e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3044,6 +3044,11 @@ dependencies: "@babel/types" "^7.20.7" +"@types/bcryptjs@^2.4.6": + version "2.4.6" + resolved "https://registry.yarnpkg.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz#2b92e3c2121c66eba3901e64faf8bb922ec291fa" + integrity sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ== + "@types/eslint@^7.29.0": version "7.29.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.29.0.tgz#e56ddc8e542815272720bb0b4ccc2aff9c3e1c78" @@ -4605,6 +4610,11 @@ batch@0.6.1: resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== +bcryptjs@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" + integrity sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ== + bfj@^7.0.2: version "7.1.0" resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.1.0.tgz#c5177d522103f9040e1b12980fe8c38cf41d3f8b"