Skip to content

Commit

Permalink
Merge pull request #120 from weaponsforge/dev
Browse files Browse the repository at this point in the history
v2.0.2
  • Loading branch information
weaponsforge authored Apr 27, 2023
2 parents 0999d63 + 293f5a9 commit 068dacc
Show file tree
Hide file tree
Showing 16 changed files with 127 additions and 47 deletions.
21 changes: 19 additions & 2 deletions client/src/common/ui/fileuploadbutton/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -124,6 +140,7 @@ function FileUploadButton ({
hidden
accept="image/*"
type="file"
ref={fileRef}
onChange={setSelectedFile}
/>
<IconPicture sx={{ color: 'black' }} />
Expand Down
26 changes: 26 additions & 0 deletions client/src/components/account/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const messages = [
{
mode: 'verifyEmail',
message: 'Please wait while we verify your email.',
success: `Success! Email verified. You can now <a href="${process.env.NEXT_PUBLIC_BASE_PATH}/login">sign in</a> with your new account.`,
error: `Try verifying your email again.<br>Your request to verify your email has expired or the link has already been used.<br><br>Resend email verification? <a href="${process.env.NEXT_PUBLIC_BASE_PATH}/account?mode=resend_email_verification">Resend</a>`
},
{
mode: 'resetPassword',
message: 'Reset your password.',
success: `Success! Password changed. You can now <a href="${process.env.NEXT_PUBLIC_BASE_PATH}/login">sign in</a> using your new password.`,
error: `Try <a href="${process.env.NEXT_PUBLIC_BASE_PATH}/recoverPassword">resetting</a> your password again.<br>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
21 changes: 20 additions & 1 deletion client/src/components/recoverPassword/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -38,7 +39,8 @@ const RecoverPasswordComponent = ({state, eventsHandler}) => {
gridTemplateColumns: '1fr 20px',
gridTemplateAreas:
`"username icon1"
"recoverPassword ."`,
"recoverPassword ."
"resend ."`,
alignItems:'stretch',
gap: '10px',
minWidth: '300px',
Expand Down Expand Up @@ -94,6 +96,23 @@ const RecoverPasswordComponent = ({state, eventsHandler}) => {
/>
}

{(state?.message?.includes('User is not yet email-verified')) &&
<Typography
sx={{
fontSize: '12px',
textAlign: 'center',
marginTop: '-5px',
color:theme.palette.text.primary,
a: {
color:theme.palette.text.primary
}
}}
style={{gridArea: 'resend'}}
>
Did not receive the account verification email? Resend it &nbsp;
<Link href="/account?mode=resend_email_verification">here</Link>.
</Typography>
}
</Paper>
</Paper>

Expand Down
4 changes: 2 additions & 2 deletions client/src/components/register/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Page>
Expand Down Expand Up @@ -147,7 +147,7 @@ const RegisterComponent = ({ state, eventsHandler }) => {
</Typography>
</Link>

{(errorMessage?.includes('auth/email-already-in-use')) &&
{(!loading && initialized) &&
<Typography
sx={{
fontSize: '12px',
Expand Down
2 changes: 1 addition & 1 deletion client/src/domain/account/resetpassword.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function ResetPasswordComponent ({ loading, locked, handleResetPasswordSubmit })
password.error ||
confirmpassword.error ||
password.value === '' ||
confirmpassword === '')
confirmpassword.value === '')
}>
Submit
</Button>
Expand Down
30 changes: 22 additions & 8 deletions client/src/lib/hooks/useFetchContactPhoto.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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}
Expand All @@ -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
Expand All @@ -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])

Expand All @@ -53,6 +67,6 @@ export default function useFetchContactPhoto (storageFilePath) {
data: storagePhotoFile.data,
loading: storagePhotoFile.loading,
error: errorString,
photo
photo: objectURL
}
}
2 changes: 1 addition & 1 deletion client/src/pages/account/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 0 additions & 24 deletions client/src/pages/account/messages.json

This file was deleted.

2 changes: 1 addition & 1 deletion client/src/pages/recoverPassword/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
6 changes: 4 additions & 2 deletions client/src/pages/register/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const defaultState = {
},
errorMessage: undefined,
successMessage: '',
loading: false
loading: false,
initialized: false
}

const Register = () => {
Expand Down Expand Up @@ -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.'
: ''
}))
})()
Expand Down
7 changes: 5 additions & 2 deletions server/src/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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
14 changes: 14 additions & 0 deletions server/src/middleware/attachaccesscontrolheader.js
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions server/src/middleware/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const validToken = require('./validtoken')
const attachAccessControllAllowOrigin = require('./attachaccesscontrolheader')

module.exports = {
validToken,
attachAccessControllAllowOrigin
}
4 changes: 3 additions & 1 deletion server/src/middleware/validtoken.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ')) &&
Expand Down Expand Up @@ -47,3 +47,5 @@ module.exports.validToken = async (req, res, next) => {
return res.status(403).send('Unauthorized')
}
}

module.exports = validToken
2 changes: 1 addition & 1 deletion server/src/modules/contact/exportcsv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) => ({
Expand Down
2 changes: 1 addition & 1 deletion server/src/modules/contact/exportpdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: {
Expand Down

0 comments on commit 068dacc

Please sign in to comment.