diff --git a/docs/nginx.conf b/docs/nginx.conf index 73a085d..0b70e07 100644 --- a/docs/nginx.conf +++ b/docs/nginx.conf @@ -83,7 +83,7 @@ server { default_type application/xml; } - location ~ ^/(datenschutz|impressum|batteriegesetzhinweise|widerrufsrecht|sitemap|agb|Kategorien|Konfigurator|404|profile|resetPassword|thc-test|filiale|aktionen|presseverleih|payment/success)(/|$) { + location ~ ^/(datenschutz|impressum|batteriegesetzhinweise|widerrufsrecht|sitemap|agb|Kategorien|Konfigurator|404|profile|resetPassword|thc-test|linkTelegram|filiale|aktionen|presseverleih|payment/success)(/|$) { types {} default_type text/html; } diff --git a/prerender.cjs b/prerender.cjs index aacc549..30350cb 100644 --- a/prerender.cjs +++ b/prerender.cjs @@ -424,6 +424,11 @@ const renderApp = async (categoryData, socket) => { const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword"); fs.copyFileSync(indexPath, resetPasswordPath); console.log(`✅ Copied index.html to ${resetPasswordPath}`); + + // Copy index.html to linkTelegram (no file extension) for SPA routing + const linkTelegramPath = path.resolve(__dirname, config.outputDir, "linkTelegram"); + fs.copyFileSync(indexPath, linkTelegramPath); + console.log(`✅ Copied index.html to ${linkTelegramPath}`); } // Render static pages diff --git a/src/App.js b/src/App.js index a402362..214816d 100644 --- a/src/App.js +++ b/src/App.js @@ -41,6 +41,7 @@ import ProductDetail from "./components/ProductDetail.js"; // Lazy load rarely-accessed pages const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js")); const ResetPassword = lazy(() => import(/* webpackChunkName: "reset-password" */ "./pages/ResetPassword.js")); +const LinkTelegramPage = lazy(() => import(/* webpackChunkName: "link-telegram" */ "./pages/LinkTelegramPage.js")); // Lazy load admin pages - only loaded when admin users access them const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js")); @@ -277,6 +278,9 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => { {/* Profile page */} } /> + {/* Link Telegram id (expects ?id=... or /linkTelegram/:id) */} + } /> + } /> {/* Payment success page for Mollie redirects */} } /> diff --git a/src/components/LoginComponent.js b/src/components/LoginComponent.js index d0fb9a0..a2f6656 100644 --- a/src/components/LoginComponent.js +++ b/src/components/LoginComponent.js @@ -240,7 +240,15 @@ export class LoginComponent extends Component { isAdmin: !!response.user.admin }); - const redirectTo = location && location.hash ? `/profile${location.hash}` : '/profile'; + const redirectTo = (() => { + // If we started login from the linkTelegram flow, come back there after auth. + // This prevents LinkTelegramPage from getting unmounted before the socket emit runs. + if (location?.pathname && location.pathname.startsWith('/linkTelegram')) { + return `${location.pathname}${location.search || ''}${location.hash || ''}`; + } + + return location && location.hash ? `/profile${location.hash}` : '/profile'; + })(); const dispatchLoginEvent = () => { window.dispatchEvent(new CustomEvent('userLoggedIn')); navigate(redirectTo); @@ -415,7 +423,14 @@ export class LoginComponent extends Component { user: response.user }); - const redirectTo = location && location.hash ? `/profile${location.hash}` : '/profile'; + const redirectTo = (() => { + // If we started login from the linkTelegram flow, come back there after auth. + if (location?.pathname && location.pathname.startsWith('/linkTelegram')) { + return `${location.pathname}${location.search || ''}${location.hash || ''}`; + } + + return location && location.hash ? `/profile${location.hash}` : '/profile'; + })(); const dispatchLoginEvent = () => { window.dispatchEvent(new CustomEvent('userLoggedIn')); navigate(redirectTo); diff --git a/src/pages/LinkTelegramPage.js b/src/pages/LinkTelegramPage.js new file mode 100644 index 0000000..885b612 --- /dev/null +++ b/src/pages/LinkTelegramPage.js @@ -0,0 +1,238 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useLocation, useNavigate, useParams, Navigate } from 'react-router-dom'; +import { + Alert, + Box, + Button, + CircularProgress, + Container, + Paper, + Typography, +} from '@mui/material'; + +import LoginComponent from '../components/LoginComponent.js'; + +const LinkTelegramPage = () => { + const location = useLocation(); + const navigate = useNavigate(); + const params = useParams(); + + const idFromQuery = useMemo(() => { + const urlParams = new URLSearchParams(location.search); + return urlParams.get('id'); + }, [location.search]); + + const id = idFromQuery || params.id || null; + + const [user, setUser] = useState(null); + const [authToken, setAuthToken] = useState(() => + typeof window === 'undefined' ? null : sessionStorage.getItem('authToken') + ); + const [loadingAuth, setLoadingAuth] = useState(true); + const [showLogin, setShowLogin] = useState(false); + + const [linking, setLinking] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + + const attemptedKeyRef = useRef(''); + + const handleLoginClose = () => { + setShowLogin(false); + + const storedUser = sessionStorage.getItem('user'); + if (!storedUser) { + navigate('/', { replace: true }); + } + }; + + useEffect(() => { + let isMounted = true; + + const loadUserFromSession = () => { + const storedUser = sessionStorage.getItem('user'); + if (!storedUser) return null; + try { + return JSON.parse(storedUser); + } catch (e) { + console.error('Error parsing user from sessionStorage:', e); + return null; + } + }; + + const checkAuth = () => { + const token = sessionStorage.getItem('authToken'); + setAuthToken(token); + + const userData = loadUserFromSession(); + if (userData) { + setUser(userData); + setShowLogin(false); + setLoadingAuth(false); + return; + } + + // If we have a token but no user yet, silently restore session via verifyToken. + if (token && window.socketManager) { + setLoadingAuth(true); + window.socketManager.emit('verifyToken', { token }, (res) => { + if (!isMounted) return; + + if (res?.success && res?.user) { + try { + sessionStorage.setItem('user', JSON.stringify(res.user)); + } catch (e) { + console.error('Failed to persist verified user:', e); + } + setUser(res.user); + setShowLogin(false); + } else { + setUser(null); + setShowLogin(true); + } + setLoadingAuth(false); + }); + return; + } + + // No user + no token => need login. + setUser(null); + setShowLogin(true); + setLoadingAuth(false); + }; + + checkAuth(); + + const onUserLoggedIn = () => checkAuth(); + window.addEventListener('userLoggedIn', onUserLoggedIn); + + const handleStorageChange = (e) => { + if (e.key === 'user' && !e.newValue) { + setShowLogin(true); + setUser(null); + } + if (e.key === 'authToken') { + checkAuth(); + } + }; + window.addEventListener('storage', handleStorageChange); + + return () => { + isMounted = false; + window.removeEventListener('userLoggedIn', onUserLoggedIn); + window.removeEventListener('storage', handleStorageChange); + }; + }, []); + + useEffect(() => { + if (!user) return; + if (!id) { + setError('Missing Telegram id.'); + setSuccess(false); + return; + } + if (!window.socketManager) return; + + const attemptKey = `${user?.id || 'anon'}:${id}:${authToken || ''}`; + if (attemptedKeyRef.current === attemptKey) return; + attemptedKeyRef.current = attemptKey; + + if (!authToken) { + setError('Not authenticated (missing auth token).'); + setSuccess(false); + setShowLogin(true); + return; + } + + setLinking(true); + setError(''); + setSuccess(false); + + // 1) Verify token so server-side socket flags are set. + window.socketManager.emit('verifyToken', { token: authToken }, (verifyRes) => { + if (!verifyRes?.success) { + setLinking(false); + setError(verifyRes?.message || 'Not authenticated.'); + setShowLogin(true); + return; + } + + // 2) Link Telegram id after authentication. + window.socketManager.emit('linkTelegram', { id }, (linkRes) => { + setLinking(false); + + if (linkRes?.success) { + setSuccess(true); + setError(''); + return; + } + + setSuccess(false); + setError( + linkRes?.error || linkRes?.message || 'Failed to link Telegram.' + ); + }); + }); + }, [id, user, authToken]); + + if (showLogin) { + return ; + } + + if (loadingAuth) { + return ( + + + + ); + } + + if (!user) { + return ; + } + + return ( + + + + Telegram verknüpfen + + + {error ? ( + + {error} + + ) : null} + + {success ? ( + + Telegram wurde erfolgreich verknüpft. + + ) : null} + + {(!success && !error) || linking ? ( + + + Bitte warten, wir verifizieren und verknüpfen dein Telegram. + + {linking ? : null} + + ) : null} + + + + + + + ); +}; + +export default LinkTelegramPage; +