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;
+