Skip to content

Commit

Permalink
Fix reCAPTCHA in Safari and other issues
Browse files Browse the repository at this point in the history
  • Loading branch information
aelassas committed Dec 14, 2024
1 parent a0fd3d4 commit 2866cd3
Show file tree
Hide file tree
Showing 22 changed files with 245 additions and 195 deletions.
21 changes: 12 additions & 9 deletions api/src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1539,15 +1539,18 @@ export const verifyRecaptcha = async (req: Request, res: Response) => {
*/
export const sendEmail = async (req: Request, res: Response) => {
try {
const { body }: { body: bookcarsTypes.SendEmailPayload } = req
const { from, to, subject, message, recaptchaToken: token, ip, isContactForm } = body
const result = await axios.get(`https://www.google.com/recaptcha/api/siteverify?secret=${encodeURIComponent(env.RECAPTCHA_SECRET)}&response=${encodeURIComponent(token)}&remoteip=${ip}`)
const { success } = result.data

if (!success) {
return res.sendStatus(400)
const whitelist = [
helper.trimEnd(env.BACKEND_HOST, '/'),
helper.trimEnd(env.FRONTEND_HOST, '/'),
]
const { origin } = req.headers
if (!origin || whitelist.indexOf(helper.trimEnd(origin, '/')) === -1) {
throw new Error('Unauthorized!')
}

const { body }: { body: bookcarsTypes.SendEmailPayload } = req
const { from, to, subject, message, isContactForm } = body

const mailOptions: nodemailer.SendMailOptions = {
from: env.SMTP_FROM,
to,
Expand All @@ -1563,8 +1566,8 @@ export const sendEmail = async (req: Request, res: Response) => {

return res.sendStatus(200)
} catch (err) {
logger.error(`[user.delete] ${i18n.t('DB_ERROR')} ${JSON.stringify(req.body)}`, err)
return res.status(400).send(i18n.t('DB_ERROR') + err)
logger.error(`[user.sendEmail] ${JSON.stringify(req.body)}`, err)
return res.status(400).send(err)
}
}

Expand Down
2 changes: 1 addition & 1 deletion api/src/routes/bookingRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ routes.route(routeNames.update).put(authJwt.verifyToken, bookingController.updat
routes.route(routeNames.updateStatus).post(authJwt.verifyToken, bookingController.updateStatus)
routes.route(routeNames.delete).post(authJwt.verifyToken, bookingController.deleteBookings)
routes.route(routeNames.deleteTempBooking).delete(bookingController.deleteTempBooking)
routes.route(routeNames.getBooking).get(authJwt.verifyToken, bookingController.getBooking)
routes.route(routeNames.getBooking).get(bookingController.getBooking)
routes.route(routeNames.getBookingId).get(bookingController.getBookingId)
routes.route(routeNames.getBookings).post(authJwt.verifyToken, bookingController.getBookings)
routes.route(routeNames.hasBookings).get(authJwt.verifyToken, bookingController.hasBookings)
Expand Down
27 changes: 15 additions & 12 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="preload" as="image" href="/cover.png">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="BookCars Rental Service" />
<title>BookCars Rental Service</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="preload" as="image" href="/cover.png">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="BookCars Rental Service" />
<title>BookCars Rental Service</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

</html>
14 changes: 0 additions & 14 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"react-circle-flags": "^0.0.23",
"react-dom": "^19.0.0",
"react-ga4": "^2.1.0",
"react-google-recaptcha-v3": "^1.10.1",
"react-leaflet": "^5.0.0-rc.2",
"react-localization": "^1.0.19",
"react-router-dom": "^7.0.2",
Expand Down
65 changes: 31 additions & 34 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import SuspenseRouter from '@/components/SuspenseRouter'
import env from '@/config/env.config'
import { GlobalProvider } from '@/context/GlobalContext'
import { init as initGA } from '@/common/ga4'
import ReCaptchaProvider from '@/components/ReCaptchaProvider'
import ScrollToTop from '@/components/ScrollToTop'

if (env.GOOGLE_ANALYTICS_ENABLED) {
Expand Down Expand Up @@ -36,41 +35,39 @@ const Faq = lazy(() => import('@/pages/Faq'))

const App = () => (
<GlobalProvider>
<ReCaptchaProvider>
<SuspenseRouter window={window}>
<ScrollToTop />
<SuspenseRouter window={window}>
<ScrollToTop />

<div className="app">
<Suspense fallback={<></>}>
<Routes>
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/activate" element={<Activate />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/checkout-session/:sessionId" element={<CheckoutSession />} />
<Route path="/bookings" element={<Bookings />} />
<Route path="/booking" element={<Booking />} />
<Route path="/settings" element={<Settings />} />
<Route path="/notifications" element={<Notifications />} />
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/about" element={<About />} />
<Route path="/tos" element={<ToS />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/contact" element={<Contact />} />
<Route path="/locations" element={<Locations />} />
<Route path="/suppliers" element={<Suppliers />} />
<Route path="/faq" element={<Faq />} />
<div className="app">
<Suspense fallback={<></>}>
<Routes>
<Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-up" element={<SignUp />} />
<Route path="/activate" element={<Activate />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/" element={<Home />} />
<Route path="/search" element={<Search />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/checkout-session/:sessionId" element={<CheckoutSession />} />
<Route path="/bookings" element={<Bookings />} />
<Route path="/booking" element={<Booking />} />
<Route path="/settings" element={<Settings />} />
<Route path="/notifications" element={<Notifications />} />
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/about" element={<About />} />
<Route path="/tos" element={<ToS />} />
<Route path="/privacy" element={<Privacy />} />
<Route path="/contact" element={<Contact />} />
<Route path="/locations" element={<Locations />} />
<Route path="/suppliers" element={<Suppliers />} />
<Route path="/faq" element={<Faq />} />

<Route path="*" element={<NoMatch />} />
</Routes>
</Suspense>
</div>
</SuspenseRouter>
</ReCaptchaProvider>
<Route path="*" element={<NoMatch />} />
</Routes>
</Suspense>
</div>
</SuspenseRouter>
</GlobalProvider>
)

Expand Down
5 changes: 2 additions & 3 deletions frontend/src/assets/css/home.css
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,8 @@ div.home div.customer-care div.customer-care-img img {
}

div.home div.search div.home-search {
width: calc(100% - 40px) !important;
padding: 20px;
margin-top: -187px;
padding: 20px 10px;
margin: -187px 20px 0 20px;
}

div.home div.why div.why-boxes,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/assets/css/search.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ div.search div.col-1 .filter {
justify-content: center;
}

div.search .btn-filters {
max-width: 480px;
margin-top: 5px;
}

/* Device width is less than or equal to 960px */

@media only screen and (width <=960px) {
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/common/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { strings } from '@/lang/cars'
import { strings as commonStrings } from '@/lang/common'
import env from '@/config/env.config'
import * as StripeService from '@/services/StripeService'
import * as UserService from '@/services/UserService'

/**
* Get language.
Expand Down Expand Up @@ -638,3 +639,22 @@ export const downloadURI = (uri: string, name: string = '') => {
link.click()
link.remove()
}

/**
* Verify reCAPTCHA token.
*
* @async
* @param {string} token
* @returns {Promise<boolean>}
*/
export const verifyReCaptcha = async (token: string): Promise<boolean> => {
try {
const ip = await UserService.getIP()
const status = await UserService.verifyRecaptcha(token, ip)
const valid = status === 200
return valid
} catch (err) {
error(err)
return false
}
}
4 changes: 3 additions & 1 deletion frontend/src/components/CheckoutStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ const CheckoutStatus = (
<Toast
title={strings.CONGRATULATIONS}
text={success
? payLater ? strings.SUCCESS_PAY_LATER : strings.SUCCESS
? payLater
? strings.SUCCESS_PAY_LATER
: strings.SUCCESS
: strings.ERROR}
status={status}
/>
Expand Down
42 changes: 19 additions & 23 deletions frontend/src/components/ContactForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import { GoogleReCaptcha } from 'react-google-recaptcha-v3'
import React, { useEffect, useState } from 'react'
import {
OutlinedInput,
InputLabel,
Expand All @@ -16,6 +15,8 @@ import env from '@/config/env.config'
import { strings as commonStrings } from '@/lang/common'
import { strings } from '@/lang/contact-form'
import * as UserService from '@/services/UserService'
import useReCaptcha from '@/hooks/useRecaptcha'

import * as helper from '@/common/helper'

import '@/assets/css/contact-form.css'
Expand All @@ -27,14 +28,13 @@ interface ContactFormProps {

const ContactForm = ({ user, className }: ContactFormProps) => {
const navigate = useNavigate()
const { reCaptchaLoaded, generateReCaptchaToken } = useReCaptcha()

const [email, setEmail] = useState('')
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [emailValid, setEmailValid] = useState(true)
const [subject, setSubject] = useState('')
const [message, setMessage] = useState('')
const [recaptchaToken, setRecaptchaToken] = useState('')
const [refreshReCaptcha, setRefreshReCaptcha] = useState(false)
const [sending, setSending] = useState(false)

useEffect(() => {
Expand Down Expand Up @@ -74,29 +74,33 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
setMessage(e.target.value)
}

const handleRecaptchaVerify = useCallback(async (token: string) => {
setRecaptchaToken(token)
}, [])

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
try {
setSending(true)
e.preventDefault()
setSending(true)

const _emailValid = await validateEmail(email)
if (!_emailValid) {
return
}

const ip = await UserService.getIP()
let recaptchaToken = ''
if (reCaptchaLoaded) {
recaptchaToken = await generateReCaptchaToken()
if (!(await helper.verifyReCaptcha(recaptchaToken))) {
recaptchaToken = ''
}

if (!recaptchaToken) {
helper.error('reCAPTCHA error')
}
}

const payload: bookcarsTypes.SendEmailPayload = {
from: email,
to: env.CONTACT_EMAIL,
subject,
message,
recaptchaToken,
ip,
isContactForm: true,
}
const status = await UserService.sendEmail(payload)
Expand All @@ -115,7 +119,6 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
helper.error(err)
} finally {
setSending(false)
setRefreshReCaptcha((refresh) => !refresh)
}
}

Expand Down Expand Up @@ -170,15 +173,8 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
/>
</FormControl>

<div className="recaptcha">
<GoogleReCaptcha
refreshReCaptcha={refreshReCaptcha}
onVerify={handleRecaptchaVerify}
/>
</div>

<div className="buttons">
<Button type="submit" variant="contained" className="btn-primary btn-margin-bottom btn" size="small" disabled={sending}>
<Button type="submit" variant="contained" className="btn-primary btn-margin-bottom btn" aria-label="Send" disabled={sending}>
{
sending
? <CircularProgress color="inherit" size={24} />
Expand All @@ -188,8 +184,8 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
<Button
variant="outlined"
color="primary"
className="btn-secondary btn-margin-bottom btn"
size="small"
className="btn-margin-bottom btn"
aria-label="Cancel"
onClick={() => {
navigate('/')
}}
Expand Down
Loading

0 comments on commit 2866cd3

Please sign in to comment.