diff --git a/api/src/controllers/userController.ts b/api/src/controllers/userController.ts
index 73dc9c108..a68ac6c6b 100644
--- a/api/src/controllers/userController.ts
+++ b/api/src/controllers/userController.ts
@@ -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,
@@ -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)
}
}
diff --git a/api/src/routes/bookingRoutes.ts b/api/src/routes/bookingRoutes.ts
index 1f847f4c1..7a472a599 100644
--- a/api/src/routes/bookingRoutes.ts
+++ b/api/src/routes/bookingRoutes.ts
@@ -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)
diff --git a/frontend/index.html b/frontend/index.html
index c584ad38e..18b57a264 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1,15 +1,18 @@
-
-
-
-
-
-
- BookCars Rental Service
-
-
-
-
-
+
+
+
+
+
+
+
+ BookCars Rental Service
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 40ddb838e..25a3c15b1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -41,7 +41,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",
@@ -5939,19 +5938,6 @@
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==",
"license": "MIT"
},
- "node_modules/react-google-recaptcha-v3": {
- "version": "1.10.1",
- "resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz",
- "integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==",
- "license": "MIT",
- "dependencies": {
- "hoist-non-react-statics": "^3.3.2"
- },
- "peerDependencies": {
- "react": "^16.3 || ^17.0 || ^18.0",
- "react-dom": "^17.0 || ^18.0"
- }
- },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index d5c9a0430..30aa1ee13 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3b7865f31..5e816561b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -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) {
@@ -36,41 +35,39 @@ const Faq = lazy(() => import('@/pages/Faq'))
const App = () => (
-
-
-
+
+
-
-
>}>
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+
+ >}>
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
- } />
-
-
-
-
-
+ } />
+
+
+
+
)
diff --git a/frontend/src/assets/css/home.css b/frontend/src/assets/css/home.css
index 0cc80b702..c34c09481 100644
--- a/frontend/src/assets/css/home.css
+++ b/frontend/src/assets/css/home.css
@@ -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,
diff --git a/frontend/src/assets/css/search.css b/frontend/src/assets/css/search.css
index 9d99e319e..58ed87e4d 100644
--- a/frontend/src/assets/css/search.css
+++ b/frontend/src/assets/css/search.css
@@ -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) {
diff --git a/frontend/src/common/helper.ts b/frontend/src/common/helper.ts
index 2bfec7c79..c4fb18ed7 100644
--- a/frontend/src/common/helper.ts
+++ b/frontend/src/common/helper.ts
@@ -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.
@@ -638,3 +639,22 @@ export const downloadURI = (uri: string, name: string = '') => {
link.click()
link.remove()
}
+
+/**
+ * Verify reCAPTCHA token.
+ *
+ * @async
+ * @param {string} token
+ * @returns {Promise}
+ */
+export const verifyReCaptcha = async (token: string): Promise => {
+ 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
+ }
+}
diff --git a/frontend/src/components/CheckoutStatus.tsx b/frontend/src/components/CheckoutStatus.tsx
index d94953756..24f2d3498 100644
--- a/frontend/src/components/CheckoutStatus.tsx
+++ b/frontend/src/components/CheckoutStatus.tsx
@@ -63,7 +63,9 @@ const CheckoutStatus = (
diff --git a/frontend/src/components/ContactForm.tsx b/frontend/src/components/ContactForm.tsx
index cf1b8d03e..ae25953e9 100644
--- a/frontend/src/components/ContactForm.tsx
+++ b/frontend/src/components/ContactForm.tsx
@@ -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,
@@ -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'
@@ -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(() => {
@@ -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) => {
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)
@@ -115,7 +119,6 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
helper.error(err)
} finally {
setSending(false)
- setRefreshReCaptcha((refresh) => !refresh)
}
}
@@ -170,15 +173,8 @@ const ContactForm = ({ user, className }: ContactFormProps) => {
/>
-
-
-
-
-