diff --git a/src/components/Sorting.tsx b/src/components/Sorting.tsx index 02c7ec0..941fc6f 100644 --- a/src/components/Sorting.tsx +++ b/src/components/Sorting.tsx @@ -19,7 +19,7 @@ interface OrderItem { interface Props { orderByList: Array; - order: string; + order: 'asc' | 'desc'; setOrder: (s: 'asc' | 'desc') => void; orderBy: T; setOrderBy: (s: T) => void; diff --git a/src/components/UI/Pagination/Pagination.tsx b/src/components/UI/Pagination/Pagination.tsx index 77ac00c..4523900 100644 --- a/src/components/UI/Pagination/Pagination.tsx +++ b/src/components/UI/Pagination/Pagination.tsx @@ -12,6 +12,8 @@ export const Pagination: FC = ({ pageCount, forcePage, }) => { + const safeForcePage = pageCount > 0 ? Math.min(forcePage, pageCount - 1) : undefined; + return ( = ({ previousLabel="← Prev" pageRangeDisplayed={2} pageCount={pageCount} - forcePage={forcePage} + forcePage={safeForcePage} renderOnZeroPageCount={null} activeClassName="text-brand-500 font-bold px-3 py-2 rounded" pageClassName="px-2 py-1 sm:px-3 sm:py-2 hover:bg-gray-200" diff --git a/src/components/modal/SettingsTutorialModal/components/HubspotForm.tsx b/src/components/modal/SettingsTutorialModal/components/HubspotForm.tsx index 95da5ea..95e8a0d 100644 --- a/src/components/modal/SettingsTutorialModal/components/HubspotForm.tsx +++ b/src/components/modal/SettingsTutorialModal/components/HubspotForm.tsx @@ -17,6 +17,14 @@ declare global { export const HubspotForm = () => { useEffect(() => { + const enabled = String(import.meta.env.VITE_HUBSPOT_ENABLED || '').toLowerCase() === 'true'; + const portalId = String(import.meta.env.VITE_HUBSPOT_PORTAL_ID || '').trim(); + const formId = String(import.meta.env.VITE_HUBSPOT_FORM_ID_TUTORIAL || '').trim(); + const region = String(import.meta.env.VITE_HUBSPOT_REGION || 'na1').trim(); + + // Enterprise/self-hosted safety: do not load HubSpot unless explicitly enabled + configured + if (!enabled || !portalId || !formId) return; + const script = document.createElement('script'); script.src = 'https://js.hsforms.net/forms/embed/v2.js'; script.async = true; @@ -25,9 +33,9 @@ export const HubspotForm = () => { script.onload = () => { if (window.hbspt) { window.hbspt.forms.create({ - region: 'na1', - portalId: '4732608', - formId: '86cdc2e8-2221-44b0-a926-02bec92c1bed', + region, + portalId, + formId, target: '#hubspot-form-wrapper' }); } diff --git a/src/hooks/useCentrifuge.ts b/src/hooks/useCentrifuge.ts index b0fc707..3edc371 100644 --- a/src/hooks/useCentrifuge.ts +++ b/src/hooks/useCentrifuge.ts @@ -1,8 +1,19 @@ import { useEffect, useState } from 'react'; import { Centrifuge } from 'centrifuge'; -import { useAppStore } from '../store/useAppStore';import { refreshToken } from '../http'; +import { refreshToken } from '../http'; +import { useAppStore } from '../store/useAppStore'; -const VITE_APP_CENTRIFUGE_SERVICE = import.meta.env.VITE_APP_CENTRIFUGE_SERVICE; +function getDefaultCentrifugeEndpoint() { + if (typeof window === 'undefined') { + return ''; + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/connection/websocket`; +} + +const VITE_APP_CENTRIFUGE_SERVICE = + import.meta.env.VITE_APP_CENTRIFUGE_SERVICE || getDefaultCentrifugeEndpoint(); type CounterType = | 'counter_chats' @@ -32,6 +43,11 @@ export function useCentrifugeChannel() { } useEffect(() => { + if (!VITE_APP_CENTRIFUGE_SERVICE) { + setConnected(false); + return; + } + const token = currentUser?.wsToken; const centrifuge = new Centrifuge(VITE_APP_CENTRIFUGE_SERVICE, { diff --git a/src/pages/AdminApps.tsx b/src/pages/AdminApps.tsx index 7adc9f1..bf8bd94 100644 --- a/src/pages/AdminApps.tsx +++ b/src/pages/AdminApps.tsx @@ -44,7 +44,7 @@ export default function AdminApps() { [searchParams] ); const orderBy = useMemo( - () => (searchParams.get('orderBy') as OrderByType) || 'totalRegistered', + () => (searchParams.get('orderBy') as OrderByType) || 'createdAt', [searchParams] ); @@ -173,8 +173,6 @@ export default function AdminApps() { setShowModal(!apps.length); }, [apps.length]); - console.log('newShowModal', newShowModal); - useEffect(() => { fetchApps(); }, [fetchApps]); diff --git a/src/pages/AuthPage/FacebookButton.tsx b/src/pages/AuthPage/FacebookButton.tsx index 113c4d9..642e401 100644 --- a/src/pages/AuthPage/FacebookButton.tsx +++ b/src/pages/AuthPage/FacebookButton.tsx @@ -14,6 +14,10 @@ import CustomButton from './Button.tsx'; import { getUserCredsFromFacebook } from './firebase'; import FacebookIcon from './Icons/socials/facebookIcon'; +const HUBSPOT_ENABLED = String(import.meta.env.VITE_HUBSPOT_ENABLED || '').toLowerCase() === 'true'; +const HUBSPOT_PORTAL_ID = String(import.meta.env.VITE_HUBSPOT_PORTAL_ID || '').trim(); +const HUBSPOT_FORM_ID_SIGNUP = String(import.meta.env.VITE_HUBSPOT_FORM_ID_SIGNUP || '').trim(); + export const FacebookButton = () => { const config = useAppStore((s) => s.currentApp); const navigate = useNavigate(); @@ -64,20 +68,18 @@ export const FacebookButton = () => { return; } - const hubspotData = { - fields: [ - { name: 'firstname', value: firstName }, - { name: 'lastname', value: lastName }, - { name: 'email', value: email }, - { name: 'website', value: website }, - ], - }; + if (HUBSPOT_ENABLED && HUBSPOT_PORTAL_ID && HUBSPOT_FORM_ID_SIGNUP) { + const hubspotData = { + fields: [ + { name: 'firstname', value: firstName }, + { name: 'lastname', value: lastName }, + { name: 'email', value: email }, + { name: 'website', value: website }, + ], + }; - await sendHSFormData( - '4732608', - '1bf4cbda-8d42-4bfc-8015-c41304eabf19', - hubspotData - ); + await sendHSFormData(HUBSPOT_PORTAL_ID, HUBSPOT_FORM_ID_SIGNUP, hubspotData); + } } catch (error) { console.error(error); toast.error('Social registration failed'); diff --git a/src/pages/AuthPage/GoogleButton.tsx b/src/pages/AuthPage/GoogleButton.tsx index 934d9f7..7ec0109 100644 --- a/src/pages/AuthPage/GoogleButton.tsx +++ b/src/pages/AuthPage/GoogleButton.tsx @@ -14,6 +14,13 @@ import { navigateToUserPage } from '../../utils/navigateToUserPage'; import CustomButton from './Button'; import GoogleIcon from './Icons/socials/googleIcon'; +const ROOT_DOMAIN = String(import.meta.env.VITE_ROOT_DOMAIN || '').trim(); +function setEthoraUserCookie(value: string) { + const domainPart = + ROOT_DOMAIN && ROOT_DOMAIN !== 'localhost' ? `; domain=.${ROOT_DOMAIN}` : ''; + document.cookie = `ethora_user=${value}; path=/${domainPart}; secure; samesite=lax; max-age=604800`; +} + interface GoogleButtonProps { utm?: string | null; } @@ -79,23 +86,22 @@ export const GoogleButton = ({ utm }: GoogleButtonProps) => { return; } - const hubspotData = { - fields: [ - { name: 'firstname', value: firstName }, - { name: 'lastname', value: lastName }, - { name: 'email', value: email }, - { name: 'website', value: website }, - ], - }; - - await sendHSFormData( - '4732608', - '1bf4cbda-8d42-4bfc-8015-c41304eabf19', - hubspotData - ); + const hubspotEnabled = String(import.meta.env.VITE_HUBSPOT_ENABLED || '').toLowerCase() === 'true'; + const portalId = String(import.meta.env.VITE_HUBSPOT_PORTAL_ID || '').trim(); + const formId = String(import.meta.env.VITE_HUBSPOT_FORM_ID_SIGNUP || '').trim(); + if (hubspotEnabled && portalId && formId) { + const hubspotData = { + fields: [ + { name: 'firstname', value: firstName }, + { name: 'lastname', value: lastName }, + { name: 'email', value: email }, + { name: 'website', value: website }, + ], + }; + await sendHSFormData(portalId, formId, hubspotData); + } - document.cookie = - 'ethora_user=accregred; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('accregred'); } catch (error) { console.error(error); toast.error('Social registration failed'); @@ -107,8 +113,7 @@ export const GoogleButton = ({ utm }: GoogleButtonProps) => { loginType ).then(async ({ data }) => { await actionAfterLogin(data); - document.cookie = - 'ethora_user=accregred; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('accregred'); localStorage.setItem('newUser', true.toString()); navigateToUserPage(navigate, config?.afterLoginPage); @@ -123,8 +128,7 @@ export const GoogleButton = ({ utm }: GoogleButtonProps) => { logLogin('google', data.user._id); await actionAfterLogin(data); - document.cookie = - 'ethora_user=accregred; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('accregred'); navigateToUserPage(navigate, config?.afterLoginPage); }); diff --git a/src/pages/AuthPage/Login/Steps/LoginForm.tsx b/src/pages/AuthPage/Login/Steps/LoginForm.tsx index 94a5649..17698f7 100644 --- a/src/pages/AuthPage/Login/Steps/LoginForm.tsx +++ b/src/pages/AuthPage/Login/Steps/LoginForm.tsx @@ -14,6 +14,13 @@ import CustomButton from '../../Button'; import { GoogleButton } from '../../GoogleButton'; import { MetamaskButton } from '../../MetamaskButton'; +const ROOT_DOMAIN = String(import.meta.env.VITE_ROOT_DOMAIN || '').trim(); +function setEthoraUserCookie(value: string) { + const domainPart = + ROOT_DOMAIN && ROOT_DOMAIN !== 'localhost' ? `; domain=.${ROOT_DOMAIN}` : ''; + document.cookie = `ethora_user=${value}; path=/${domainPart}; secure; samesite=lax; max-age=604800`; +} + type Inputs = { email: string; password: string; @@ -33,10 +40,8 @@ const LoginStep = () => { httpLoginWithEmail(email, password) .then(async ({ data }) => { await actionAfterLogin(data); - logLogin('email', data.user._id); - document.cookie = - 'ethora_user=1; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('1'); if (config?.afterLoginPage) { navigateToUserPage(navigate, config.afterLoginPage as string); diff --git a/src/pages/AuthPage/MetamaskButton.tsx b/src/pages/AuthPage/MetamaskButton.tsx index f3ae58b..2dc06a2 100644 --- a/src/pages/AuthPage/MetamaskButton.tsx +++ b/src/pages/AuthPage/MetamaskButton.tsx @@ -18,6 +18,13 @@ import { navigateToUserPage } from '../../utils/navigateToUserPage'; import CustomButton from './Button'; import MetamaskIcon from './Icons/socials/metamaskIcon'; +const ROOT_DOMAIN = String(import.meta.env.VITE_ROOT_DOMAIN || '').trim(); +function setEthoraUserCookie(value: string) { + const domainPart = + ROOT_DOMAIN && ROOT_DOMAIN !== 'localhost' ? `; domain=.${ROOT_DOMAIN}` : ''; + document.cookie = `ethora_user=${value}; path=/${domainPart}; secure; samesite=lax; max-age=604800`; +} + declare global { interface Window { ethereum?: any; @@ -55,8 +62,7 @@ export const MetamaskButton = ({ utm }: MetamaskButtonProps) => { toast.success('Successfully logged in with Metamask!'); - document.cookie = - 'ethora_user=accregred; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('accregred'); if (config?.afterLoginPage) { navigateToUserPage(navigate, config.afterLoginPage as string); diff --git a/src/pages/AuthPage/Register/RegisterForm.tsx b/src/pages/AuthPage/Register/RegisterForm.tsx index a00b64d..df20631 100644 --- a/src/pages/AuthPage/Register/RegisterForm.tsx +++ b/src/pages/AuthPage/Register/RegisterForm.tsx @@ -22,7 +22,16 @@ import { GoogleButton } from '../GoogleButton'; import { MetamaskButton } from '../MetamaskButton'; import SkeletonLoader from '../SkeletonLoader'; -const SITE_KEY = import.meta.env.VITE_SITE_KEY; +const SITE_KEY = (import.meta.env.VITE_SITE_KEY || '').trim(); +const TURNSTILE_ENABLED = SITE_KEY.length > 0; +const ROOT_DOMAIN = String(import.meta.env.VITE_ROOT_DOMAIN || '').trim(); + +function setEthoraUserCookie(value: string) { + const domainPart = + ROOT_DOMAIN && ROOT_DOMAIN !== 'localhost' ? `; domain=.${ROOT_DOMAIN}` : ''; + // Keep existing behavior, but avoid hardcoding ethora.com for enterprise/self-hosted installs + document.cookie = `ethora_user=${value}; path=/${domainPart}; secure; samesite=lax; max-age=604800`; +} interface FirstStepProps { isSmallDevice?: boolean; @@ -129,7 +138,10 @@ const RegisterForm: React.FC = ({ isSmallDevice = false }) => { const formData = new FormData(formRef.current!); const cfToken = formData.get('cf-turnstile-response'); - if (!cfToken || typeof cfToken !== 'string' || cfToken.trim() === '') { + // Turnstile is optional for enterprise installs; if VITE_SITE_KEY is empty we skip it. + const cfTokenValue = + typeof cfToken === 'string' ? cfToken.trim() : ''; + if (TURNSTILE_ENABLED && !cfTokenValue) { return; } @@ -137,7 +149,7 @@ const RegisterForm: React.FC = ({ isSmallDevice = false }) => { await httpRegisterWithEmailV2( email, password, - cfToken, + cfTokenValue, firstName, lastName, utmParams || '' @@ -160,9 +172,16 @@ const RegisterForm: React.FC = ({ isSmallDevice = false }) => { return; } + const hubspotEnabled = String(import.meta.env.VITE_HUBSPOT_ENABLED || '').toLowerCase() === 'true'; + const portalId = String(import.meta.env.VITE_HUBSPOT_PORTAL_ID || '').trim(); + const formId = String(import.meta.env.VITE_HUBSPOT_FORM_ID_SIGNUP || '').trim(); + if (!hubspotEnabled || !portalId || !formId) { + return; + } + await sendHSFormData( - '4732608', - '1bf4cbda-8d42-4bfc-8015-c41304eabf19', + portalId, + formId, hubspotData ); }); @@ -174,8 +193,7 @@ const RegisterForm: React.FC = ({ isSmallDevice = false }) => { httpLoginWithEmail(email, password) .then(async ({ data }) => { - document.cookie = - 'ethora_user=1; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('1'); localStorage.setItem('newUser', true.toString()); await actionAfterLogin(data); @@ -311,12 +329,14 @@ const RegisterForm: React.FC = ({ isSmallDevice = false }) => { helperText={errors.password?.message} /> - + {TURNSTILE_ENABLED && ( + + )} >; isSmallDevice?: boolean; @@ -164,9 +172,16 @@ const FirstStep: React.FC = ({ isSmallDevice = false }) => { return; } + const hubspotEnabled = String(import.meta.env.VITE_HUBSPOT_ENABLED || '').toLowerCase() === 'true'; + const portalId = String(import.meta.env.VITE_HUBSPOT_PORTAL_ID || '').trim(); + const formId = String(import.meta.env.VITE_HUBSPOT_FORM_ID_SIGNUP || '').trim(); + if (!hubspotEnabled || !portalId || !formId) { + return; + } + await sendHSFormData( - '4732608', - '1bf4cbda-8d42-4bfc-8015-c41304eabf19', + portalId, + formId, hubspotData ); }); @@ -178,8 +193,7 @@ const FirstStep: React.FC = ({ isSmallDevice = false }) => { httpLoginWithEmail(email, password) .then(async ({ data }) => { - document.cookie = - 'ethora_user=1; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('1'); await actionAfterLogin(data); diff --git a/src/pages/AuthPage/Register/Steps/ThirdStep.tsx b/src/pages/AuthPage/Register/Steps/ThirdStep.tsx index 39c1e70..b37aab8 100644 --- a/src/pages/AuthPage/Register/Steps/ThirdStep.tsx +++ b/src/pages/AuthPage/Register/Steps/ThirdStep.tsx @@ -13,6 +13,13 @@ import { navigateToUserPage } from '../../../../utils/navigateToUserPage'; import CustomButton from '../../Button'; import SkeletonLoader from '../../SkeletonLoader'; +const ROOT_DOMAIN = String(import.meta.env.VITE_ROOT_DOMAIN || '').trim(); +function setEthoraUserCookie(value: string) { + const domainPart = + ROOT_DOMAIN && ROOT_DOMAIN !== 'localhost' ? `; domain=.${ROOT_DOMAIN}` : ''; + document.cookie = `ethora_user=${value}; path=/${domainPart}; secure; samesite=lax; max-age=604800`; +} + interface Inputs { newPassword: string; repeatPassword: string; @@ -103,8 +110,7 @@ const ThirdStep = () => { httpLoginWithEmail(email, newPassword) .then(async ({ data }) => { - document.cookie = - 'ethora_user=1; path=/; domain=.ethora.com; secure; samesite=lax; max-age=604800'; + setEthoraUserCookie('1'); await actionAfterLogin(data); diff --git a/src/pages/Chat.tsx b/src/pages/Chat.tsx index 26daca2..a9538ee 100644 --- a/src/pages/Chat.tsx +++ b/src/pages/Chat.tsx @@ -20,14 +20,23 @@ const MemoizedChat = React.memo(function ChatComponent({ }: ChatComponentProps) { const appToken = useAppStore((s) => s.currentApp?.appToken); - const handleChangeTokens = async () => { - const { token, refreshToken: refresh } = await refreshToken(); + const handleChangeTokens = async (): Promise< + | { accessToken: string; refreshToken?: string | undefined } + | null + > => { + try { + const { token, refreshToken: refresh } = await refreshToken(); - localStorage.setItem('refreshToken-538', refresh); - localStorage.setItem('token-538', token); + if (refresh) localStorage.setItem('refreshToken-538', refresh); + localStorage.setItem('token-538', token); - httpTokens.token = token; - httpTokens.refreshToken = refresh; + httpTokens.token = token; + httpTokens.refreshToken = refresh; + + return { accessToken: token, refreshToken: refresh }; + } catch (e) { + return null; + } }; return (