diff --git a/client/src/common/ui/fileuploadbutton/index.js b/client/src/common/ui/fileuploadbutton/index.js
index 5d7796a..c73907e 100644
--- a/client/src/common/ui/fileuploadbutton/index.js
+++ b/client/src/common/ui/fileuploadbutton/index.js
@@ -1,5 +1,5 @@
-import { useEffect, useState, useMemo } from 'react'
-import { updateSyncV } from 'use-sync-v'
+import { useEffect, useState, useMemo, useRef } from 'react'
+import { updateSyncV, useSyncV } from 'use-sync-v'
import PropTypes from 'prop-types'
import IconButton from '@mui/material/IconButton'
@@ -45,6 +45,9 @@ function FileUploadButton ({
styles = {}
}) {
const fileId = useMemo(() => fileDomID, [fileDomID])
+ const objectData = useSyncV(fileId)
+ const fileRef = useRef()
+
const [icon, setIcon] = useState((hasFile)
? ICON_STATES.CANCEL
: ICON_STATES.SEARCH)
@@ -64,6 +67,16 @@ function FileUploadButton ({
}
}, [fileId])
+ useEffect(() => {
+ if (objectData?.imgSrc === null) {
+ return
+ }
+
+ return () => {
+ URL.revokeObjectURL(objectData?.imgSrc)
+ }
+ }, [objectData])
+
const IconPicture = useMemo(() => {
return (icon === ICON_STATES.SEARCH)
? PhotoCameraIcon
@@ -96,6 +109,9 @@ function FileUploadButton ({
const clearFile = (e) => {
e.preventDefault()
+ fileRef.current.value = null
+ URL.revokeObjectURL(objectData?.imgSrc)
+
setIcon(ICON_STATES.SEARCH)
updateSyncV(fileDomID, {
@@ -124,6 +140,7 @@ function FileUploadButton ({
hidden
accept="image/*"
type="file"
+ ref={fileRef}
onChange={setSelectedFile}
/>
diff --git a/client/src/components/account/messages.js b/client/src/components/account/messages.js
new file mode 100644
index 0000000..c55da89
--- /dev/null
+++ b/client/src/components/account/messages.js
@@ -0,0 +1,26 @@
+const messages = [
+ {
+ mode: 'verifyEmail',
+ message: 'Please wait while we verify your email.',
+ success: `Success! Email verified. You can now sign in with your new account.`,
+ error: `Try verifying your email again.
Your request to verify your email has expired or the link has already been used.
Resend email verification? Resend`
+ },
+ {
+ mode: 'resetPassword',
+ message: 'Reset your password.',
+ success: `Success! Password changed. You can now sign in using your new password.`,
+ error: `Try resetting your password again.
Your request to change your password has expired or the link has already been used.`
+ },
+ {
+ mode: 'resend_email_verification',
+ message: 'Enter your registration email',
+ success: 'Success! Email verification sent.'
+ },
+ {
+ mode: 'recoverEmail',
+ message: '',
+ success: ''
+ }
+]
+
+export default messages
diff --git a/client/src/components/recoverPassword/index.js b/client/src/components/recoverPassword/index.js
index 5b5c330..93644c0 100644
--- a/client/src/components/recoverPassword/index.js
+++ b/client/src/components/recoverPassword/index.js
@@ -1,4 +1,5 @@
import PropTypes from 'prop-types'
+import Link from 'next/link'
import Page from '@/common/layout/page'
import { Paper, Typography } from '@mui/material'
import LoadingButton from '@/common/ui/loadingbutton'
@@ -38,7 +39,8 @@ const RecoverPasswordComponent = ({state, eventsHandler}) => {
gridTemplateColumns: '1fr 20px',
gridTemplateAreas:
`"username icon1"
- "recoverPassword ."`,
+ "recoverPassword ."
+ "resend ."`,
alignItems:'stretch',
gap: '10px',
minWidth: '300px',
@@ -94,6 +96,23 @@ const RecoverPasswordComponent = ({state, eventsHandler}) => {
/>
}
+ {(state?.message?.includes('User is not yet email-verified')) &&
+
+ Did not receive the account verification email? Resend it
+ here.
+
+ }
diff --git a/client/src/components/register/index.js b/client/src/components/register/index.js
index eeb7f3a..9076bfc 100644
--- a/client/src/components/register/index.js
+++ b/client/src/components/register/index.js
@@ -12,7 +12,7 @@ import { useTheme } from '@emotion/react'
const RegisterComponent = ({ state, eventsHandler }) => {
const theme = useTheme()
- const {joke, username, password, passwordConfirmation, errorMessage, successMessage, loading } = state
+ const {joke, username, password, passwordConfirmation, errorMessage, successMessage, loading, initialized } = state
const {usernameHandler, passwordHandler, passwordConfirmationHandler, registerHandler, resetError } = eventsHandler
return (
@@ -147,7 +147,7 @@ const RegisterComponent = ({ state, eventsHandler }) => {
- {(errorMessage?.includes('auth/email-already-in-use')) &&
+ {(!loading && initialized) &&
Submit
diff --git a/client/src/lib/hooks/useFetchContactPhoto.js b/client/src/lib/hooks/useFetchContactPhoto.js
index 9ae8106..ca1290e 100644
--- a/client/src/lib/hooks/useFetchContactPhoto.js
+++ b/client/src/lib/hooks/useFetchContactPhoto.js
@@ -1,4 +1,4 @@
-import { useEffect, useMemo } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { useAsyncV, updateAsyncV } from 'use-sync-v'
import { downloadBlobFromStorage } from '@/utils/firebase/storageutils'
@@ -8,6 +8,7 @@ const PHOTO_STORE_KEY = 'savedPhotoBlob'
* Downloads a Contact photo directly as Blob from Firebase Storage, taking the defined Firebase Storage Rules into account.
* Returns the downloaded photo's local URL converted from the downloaded Blob data, or blank String ''.
* Returns the photo download error as String, or blank ''.
+ * Revokes the local object URL from storage on compoment unmount.
*
* @param {String} storageFilePath - Contact photo's full Firebase Storage file path
* @returns {Object}
@@ -18,6 +19,7 @@ const PHOTO_STORE_KEY = 'savedPhotoBlob'
*/
export default function useFetchContactPhoto (storageFilePath) {
const storagePhotoFile = useAsyncV(PHOTO_STORE_KEY)
+ const [objectURL, setObjectURL] = useState('')
useEffect(() => {
// Reset the Storage photo Blob
@@ -29,17 +31,29 @@ export default function useFetchContactPhoto (storageFilePath) {
return () => resetPhotoStore()
}, [])
- const photo = useMemo(() => {
- return (storagePhotoFile.data !== null && !storagePhotoFile.loading)
- ? URL.createObjectURL(storagePhotoFile.data)
- : ''
+ useEffect(() => {
+ if (!objectURL) {
+ return
+ }
+
+ // Revoke/clear the local URL object URL on component unmount
+ return () => {
+ URL.revokeObjectURL(objectURL)
+ }
+ }, [objectURL])
+
+ useEffect(() => {
+ // Set the local URL object URL
+ if (storagePhotoFile.data !== null && !storagePhotoFile.loading) {
+ const url = URL.createObjectURL(storagePhotoFile.data)
+ setObjectURL(url)
+ }
}, [storagePhotoFile])
useEffect(() => {
+ // Download photo
if (storageFilePath) {
updateAsyncV(PHOTO_STORE_KEY, async () => downloadBlobFromStorage(storageFilePath))
- } else {
- updateAsyncV(PHOTO_STORE_KEY, async () => null, { deleteExistingData: true })
}
}, [storageFilePath])
@@ -53,6 +67,6 @@ export default function useFetchContactPhoto (storageFilePath) {
data: storagePhotoFile.data,
loading: storagePhotoFile.loading,
error: errorString,
- photo
+ photo: objectURL
}
}
diff --git a/client/src/pages/account/index.js b/client/src/pages/account/index.js
index 129f0b8..035f7cf 100644
--- a/client/src/pages/account/index.js
+++ b/client/src/pages/account/index.js
@@ -12,7 +12,7 @@ import {
import { usePromise, RequestStatus } from '@/lib/hooks/usePromise'
import AccountComponent from '@/components/account'
-import messages from './messages'
+import messages from '@/components/account/messages'
const defaultState = {
loading: true,
diff --git a/client/src/pages/account/messages.json b/client/src/pages/account/messages.json
deleted file mode 100644
index 3bb94ba..0000000
--- a/client/src/pages/account/messages.json
+++ /dev/null
@@ -1,24 +0,0 @@
-[
- {
- "mode": "verifyEmail",
- "message": "Please wait while we verify your email.",
- "success": "Success! Email verified. You can now sign in with your new account.",
- "error": "Try verifying your email again.
Your request to verify your email has expired or the link has already been used.
Resend email verification? Resend"
- },
- {
- "mode": "resetPassword",
- "message": "Reset your password.",
- "success": "Success! Password changed. You can now sign in using your new password.",
- "error": "Try resetting your password again.
Your request to change your password has expired or the link has already been used."
- },
- {
- "mode": "resend_email_verification",
- "message": "Enter your registration email",
- "success": "Success! Email verification sent."
- },
- {
- "mode": "recoverEmail",
- "message": "",
- "success": ""
- }
-]
diff --git a/client/src/pages/recoverPassword/index.js b/client/src/pages/recoverPassword/index.js
index e7e2984..b8236f5 100644
--- a/client/src/pages/recoverPassword/index.js
+++ b/client/src/pages/recoverPassword/index.js
@@ -60,7 +60,7 @@ const RecoverPassword = () => {
...state,
loading,
message: (status === RequestStatus.SUCCESS)
- ? 'Email sent. Please check your inbox.'
+ ? 'Email sent. Please check your inbox or your Spam folder. Wait for at most 5 minutes if you do not see the email right away.'
: error
}}
eventsHandler={eventsHandler}
diff --git a/client/src/pages/register/index.js b/client/src/pages/register/index.js
index 6c9e9fd..aac5fad 100644
--- a/client/src/pages/register/index.js
+++ b/client/src/pages/register/index.js
@@ -28,7 +28,8 @@ const defaultState = {
},
errorMessage: undefined,
successMessage: '',
- loading: false
+ loading: false,
+ initialized: false
}
const Register = () => {
@@ -108,9 +109,10 @@ const Register = () => {
setState(prev=>({
...prev,
loading: false,
+ initialized: true,
errorMessage,
successMessage: (sendVerificationStatus === PromiseWrapper.STATUS.SUCCESS)
- ? 'Email sent. Please check your email.'
+ ? 'Email sent. Please check your email. Check your Spam folder or wait for at most 5 minutes if you do not see the email right away.'
: ''
}))
})()
diff --git a/server/src/controllers/index.js b/server/src/controllers/index.js
index 8274ce0..13c67ef 100644
--- a/server/src/controllers/index.js
+++ b/server/src/controllers/index.js
@@ -2,7 +2,10 @@ const { Router } = require('express')
const router = new Router()
// Middleware
-const { validToken } = require('../middleware/validtoken')
+const {
+ validToken,
+ attachAccessControllAllowOrigin
+} = require('../middleware')
// Controllers
const Email = require('./email')
@@ -263,6 +266,6 @@ router.post('/account/action', Account.manageAccount)
* document.body.removeChild(link)
*/
-router.post('/contacts/export', validToken, Contact.exportContact)
+router.post('/contacts/export', validToken, attachAccessControllAllowOrigin, Contact.exportContact)
module.exports = router
diff --git a/server/src/middleware/attachaccesscontrolheader.js b/server/src/middleware/attachaccesscontrolheader.js
new file mode 100644
index 0000000..5093e8e
--- /dev/null
+++ b/server/src/middleware/attachaccesscontrolheader.js
@@ -0,0 +1,14 @@
+const { whitelist } = require('../utils/cors_options')
+
+// Attach 'Access-Control-Allow-Origin' to the response header for whitelisted "BASE" origins.
+const attachAccessControllAllowOrigin = (req, res, next) => {
+ const origin = req.headers.origin
+
+ if (whitelist.includes(origin)) {
+ res.setHeader('Access-Control-Allow-Origin', origin)
+ }
+
+ next()
+}
+
+module.exports = attachAccessControllAllowOrigin
diff --git a/server/src/middleware/index.js b/server/src/middleware/index.js
new file mode 100644
index 0000000..a6bb9d9
--- /dev/null
+++ b/server/src/middleware/index.js
@@ -0,0 +1,7 @@
+const validToken = require('./validtoken')
+const attachAccessControllAllowOrigin = require('./attachaccesscontrolheader')
+
+module.exports = {
+ validToken,
+ attachAccessControllAllowOrigin
+}
diff --git a/server/src/middleware/validtoken.js b/server/src/middleware/validtoken.js
index b671a0b..11d2473 100644
--- a/server/src/middleware/validtoken.js
+++ b/server/src/middleware/validtoken.js
@@ -2,7 +2,7 @@ const { getAuth } = require('../utils/db')
// Inspects if the Authorization Bearer token from client belongs to a valid signed-in user.
// Injects the decoded Firebase Auth user's information to req.user if token is valid.
-module.exports.validToken = async (req, res, next) => {
+const validToken = async (req, res, next) => {
if (
(!req.headers.authorization ||
!req.headers.authorization.startsWith('Bearer ')) &&
@@ -47,3 +47,5 @@ module.exports.validToken = async (req, res, next) => {
return res.status(403).send('Unauthorized')
}
}
+
+module.exports = validToken
diff --git a/server/src/modules/contact/exportcsv.js b/server/src/modules/contact/exportcsv.js
index 6a9676c..0d60e6e 100644
--- a/server/src/modules/contact/exportcsv.js
+++ b/server/src/modules/contact/exportcsv.js
@@ -5,6 +5,7 @@ const { CONTACT_FIELDS } = require('../../utils/constants')
/**
* Exports Contacts Firestore document/s from a user's /contacts subcollection to a CSV file.
+ * Requires using the attachAccessControllAllowOrigin middleware for enhanced cross-origin security.
* @param {Object[]} contacts - Firestore Contact documents.
* @param {Object} res - Express response object.
* @returns
@@ -18,7 +19,6 @@ const exportCSV = (contacts = [], res) => {
res.setHeader('Content-Type', 'text/csv')
res.setHeader('Content-Disposition', `'attachment; filename="${filename}"'`)
- res.setHeader('Access-Control-Allow-Origin', process.env.CLIENT_WEBSITE_URL)
contacts.forEach((contact) => {
const obj = Object.values(CONTACT_FIELDS).reduce((list, key) => ({
diff --git a/server/src/modules/contact/exportpdf.js b/server/src/modules/contact/exportpdf.js
index 6588882..3eab762 100644
--- a/server/src/modules/contact/exportpdf.js
+++ b/server/src/modules/contact/exportpdf.js
@@ -19,6 +19,7 @@ const printer = new PDFPrinter(fonts)
/**
* Exports Contacts Firestore document/s from a user's /contacts subcollection to a PDF file.
+ * Requires using the attachAccessControllAllowOrigin middleware for enhanced cross-origin security.
* @param {Object[]} contacts - Firestore Contact documents.
* @param {Object} res - Express response object.
* @returns
@@ -30,7 +31,6 @@ const exportPDF = (contacts = [], res) => {
res.setHeader('Content-Type', 'application/pdf')
res.setHeader('Content-Disposition', `'attachment; filename="${filename}"'`)
- res.setHeader('Access-Control-Allow-Origin', process.env.CLIENT_WEBSITE_URL)
const docDefinition = {
defaultStyle: {