diff --git a/package-lock.json b/package-lock.json index 4684cc2..49915ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@maptiler/geocoder": "^1.1.0", "@next-auth/prisma-adapter": "^0.5.2-next.19", "@prisma/client": "^3.6.0", + "@sendgrid/mail": "^7.6.2", "@types/react": "^17.0.27", "axios": "^0.24.0", "babel-plugin-superjson-next": "^0.4.2", @@ -32,6 +33,7 @@ "@testing-library/react": "^12.1.1", "@types/faker": "5.5.9", "@types/jest": "^27.0.2", + "@types/react-dom": "^18.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", @@ -1728,6 +1730,49 @@ "integrity": "sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==", "dev": true }, + "node_modules/@sendgrid/client": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.6.2.tgz", + "integrity": "sha512-Yw3i3vPBBwfiIi+4i7+1f1rwQoLlLsu3qW16d1UuRp6RgX6H6yHYb2/PfqwNyCC0qzqIWGUKPWwYe5ggcr5Guw==", + "dependencies": { + "@sendgrid/helpers": "^7.6.2", + "axios": "^0.26.0" + }, + "engines": { + "node": "6.* || 8.* || >=10.*" + } + }, + "node_modules/@sendgrid/client/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/@sendgrid/helpers": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.6.2.tgz", + "integrity": "sha512-kGW0kM2AOHfXjcvB6Lgwa/nMv8IALu0KyNY9X4HSa3MtLohymuhbG9HgjrOh66+BkbsfA03H3bcT0+sPVJ0GKQ==", + "dependencies": { + "deepmerge": "^4.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sendgrid/mail": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.6.2.tgz", + "integrity": "sha512-IHHZFvgU95aqb11AevQvAfautj2pb8iW8UCiUJ2ae9pRF37e6EkBmU9NgdFjbQ/8Xhhm+KDVDzn/JLxDN/GiBw==", + "dependencies": { + "@sendgrid/client": "^7.6.2", + "@sendgrid/helpers": "^7.6.2" + }, + "engines": { + "node": "6.* || 8.* || >=10.*" + } + }, "node_modules/@sindresorhus/is": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", @@ -2136,6 +2181,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.0.tgz", + "integrity": "sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -3681,7 +3735,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -4996,9 +5049,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", - "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "funding": [ { "type": "individual", @@ -12298,6 +12351,42 @@ "integrity": "sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A==", "dev": true }, + "@sendgrid/client": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.6.2.tgz", + "integrity": "sha512-Yw3i3vPBBwfiIi+4i7+1f1rwQoLlLsu3qW16d1UuRp6RgX6H6yHYb2/PfqwNyCC0qzqIWGUKPWwYe5ggcr5Guw==", + "requires": { + "@sendgrid/helpers": "^7.6.2", + "axios": "^0.26.0" + }, + "dependencies": { + "axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "requires": { + "follow-redirects": "^1.14.8" + } + } + } + }, + "@sendgrid/helpers": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.6.2.tgz", + "integrity": "sha512-kGW0kM2AOHfXjcvB6Lgwa/nMv8IALu0KyNY9X4HSa3MtLohymuhbG9HgjrOh66+BkbsfA03H3bcT0+sPVJ0GKQ==", + "requires": { + "deepmerge": "^4.2.2" + } + }, + "@sendgrid/mail": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.6.2.tgz", + "integrity": "sha512-IHHZFvgU95aqb11AevQvAfautj2pb8iW8UCiUJ2ae9pRF37e6EkBmU9NgdFjbQ/8Xhhm+KDVDzn/JLxDN/GiBw==", + "requires": { + "@sendgrid/client": "^7.6.2", + "@sendgrid/helpers": "^7.6.2" + } + }, "@sindresorhus/is": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", @@ -12644,6 +12733,15 @@ "csstype": "^3.0.2" } }, + "@types/react-dom": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.0.tgz", + "integrity": "sha512-49897Y0UiCGmxZqpC8Blrf6meL8QUla6eb+BBhn69dTXlmuOlzkfr7HHY/O8J25e1lTUMs+YYxSlVDAaGHCOLg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -13827,8 +13925,7 @@ "deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" }, "defer-to-connect": { "version": "2.0.1", @@ -14839,9 +14936,9 @@ "dev": true }, "follow-redirects": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz", - "integrity": "sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A==" + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, "foreach": { "version": "2.0.5", diff --git a/package.json b/package.json index e02e306..6a9bedd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@maptiler/geocoder": "^1.1.0", "@next-auth/prisma-adapter": "^0.5.2-next.19", "@prisma/client": "^3.6.0", + "@sendgrid/mail": "^7.6.2", "@types/react": "^17.0.27", "axios": "^0.24.0", "babel-plugin-superjson-next": "^0.4.2", @@ -37,6 +38,7 @@ "@testing-library/react": "^12.1.1", "@types/faker": "5.5.9", "@types/jest": "^27.0.2", + "@types/react-dom": "^18.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", diff --git a/src/components/App/EmailConfirmTemplate/index.tsx b/src/components/App/EmailConfirmTemplate/index.tsx new file mode 100644 index 0000000..5e1e7f3 --- /dev/null +++ b/src/components/App/EmailConfirmTemplate/index.tsx @@ -0,0 +1,31 @@ +import { Button } from '../../Global'; + +const EmailConfirmTemplate = ({ + userEmail, + hash +}: { + userEmail: string; + hash: string; +}) => { + const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; + const redirectURL = `${BASE_URL}/auth/confirmEmail/${userEmail}/${hash}`; + + return ( + <> +
Redirecting...
; + } + + if (!isLoading) { + returnLoading...
; +}; + +export default ConfirmEmail; diff --git a/src/utils/confimationHash.ts b/src/utils/confimationHash.ts new file mode 100644 index 0000000..00bf027 --- /dev/null +++ b/src/utils/confimationHash.ts @@ -0,0 +1,41 @@ +import { prisma, User } from '@prisma/client'; +import { hashFunction, checkHash } from '.'; +import { getUserByEmail } from '../services/getUserByEmail'; + +const getStringToHash = (user: User) => + `${user.id}${user.email}${user.updatedAt}${user.createdAt}`; + +export const getConfirmationHash = async (userEmail: string) => { + const user: User = await getUserByEmail({ email: userEmail }); + + if (!user) { + console.error('User not found'); + return; + } + + const stringToHash = getStringToHash(user); + + return hashFunction(stringToHash); +}; + +export const decodeConfirmationHash = async ( + userEmail: string, + hash: string +) => { + const user: User = await getUserByEmail({ email: userEmail }); + + if (!user) { + console.error('User not found'); + return; + } + + const stringToHash = getStringToHash(user); + + const isConfirmed = checkHash(stringToHash, hash); + + if (isConfirmed) { + // TODO: update database + } + + return isConfirmed; +}; diff --git a/src/utils/index.tsx b/src/utils/index.tsx new file mode 100644 index 0000000..a87cde7 --- /dev/null +++ b/src/utils/index.tsx @@ -0,0 +1,9 @@ +export { difficultySentenceCase } from './difficulty'; +export { checkPuzzleAnswer } from './puzzles'; +export { hashFunction, checkHash } from './password'; +export { getRandomGifSrc, feedbackGifOptions } from './feedbackGif'; +export { handleLoadingError } from './errorHandler'; +export { handleServiceError } from './error'; +export { fetcher } from './swr'; +export { decodeConfirmationHash, getConfirmationHash } from './confimationHash'; +export { renderReactToHTML } from './renderReactToHTML'; diff --git a/src/utils/renderReactToHTML.ts b/src/utils/renderReactToHTML.ts new file mode 100644 index 0000000..45e9e97 --- /dev/null +++ b/src/utils/renderReactToHTML.ts @@ -0,0 +1,9 @@ +import * as ReactDOMServer from 'react-dom/server'; + +export const renderReactToHTML = (dom: JSX.Element, full: boolean = false) => { + let html = ReactDOMServer.renderToString(dom); + if (full) { + html = '' + html; + } + return html; +}; diff --git a/src/utils/sendEmail.ts b/src/utils/sendEmail.ts new file mode 100644 index 0000000..99e08f1 --- /dev/null +++ b/src/utils/sendEmail.ts @@ -0,0 +1,27 @@ +import sgMail from '@sendgrid/mail'; +import toast from 'react-hot-toast'; + +export const sendEmail = async ( + receiverEmail: string, + subject: string, + content: string, + mailSendSuccessToast?: string +) => { + sgMail.setApiKey(process.env.SENDGRID_API_KEY); + const sender = process.env.SENDGRID_SENDER; + const msg = { + to: receiverEmail, + from: sender, // Change to your verified sender + subject, + html: content + }; + + try { + await sgMail.send(msg); + toast.success(mailSendSuccessToast || 'Email sent successfully'); + } catch (err) { + toast.error(err.message); + } +}; + +// TODO: Find a way to test this function