feat: Add LinkTelegram page and routing; enhance login flow to support redirection from linkTelegram
This commit is contained in:
@@ -83,7 +83,7 @@ server {
|
|||||||
default_type application/xml;
|
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 {}
|
types {}
|
||||||
default_type text/html;
|
default_type text/html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,6 +424,11 @@ const renderApp = async (categoryData, socket) => {
|
|||||||
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
|
const resetPasswordPath = path.resolve(__dirname, config.outputDir, "resetPassword");
|
||||||
fs.copyFileSync(indexPath, resetPasswordPath);
|
fs.copyFileSync(indexPath, resetPasswordPath);
|
||||||
console.log(`✅ Copied index.html to ${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
|
// Render static pages
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import ProductDetail from "./components/ProductDetail.js";
|
|||||||
// Lazy load rarely-accessed pages
|
// Lazy load rarely-accessed pages
|
||||||
const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js"));
|
const ProfilePage = lazy(() => import(/* webpackChunkName: "profile" */ "./pages/ProfilePage.js"));
|
||||||
const ResetPassword = lazy(() => import(/* webpackChunkName: "reset-password" */ "./pages/ResetPassword.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
|
// Lazy load admin pages - only loaded when admin users access them
|
||||||
const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js"));
|
const AdminPage = lazy(() => import(/* webpackChunkName: "admin" */ "./pages/AdminPage.js"));
|
||||||
@@ -277,6 +278,9 @@ const AppContent = ({ currentTheme, dynamicTheme, onThemeChange }) => {
|
|||||||
|
|
||||||
{/* Profile page */}
|
{/* Profile page */}
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
{/* Link Telegram id (expects ?id=... or /linkTelegram/:id) */}
|
||||||
|
<Route path="/linkTelegram" element={<LinkTelegramPage />} />
|
||||||
|
<Route path="/linkTelegram/:id" element={<LinkTelegramPage />} />
|
||||||
|
|
||||||
{/* Payment success page for Mollie redirects */}
|
{/* Payment success page for Mollie redirects */}
|
||||||
<Route path="/payment/success" element={<PaymentSuccess />} />
|
<Route path="/payment/success" element={<PaymentSuccess />} />
|
||||||
|
|||||||
@@ -240,7 +240,15 @@ export class LoginComponent extends Component {
|
|||||||
isAdmin: !!response.user.admin
|
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 = () => {
|
const dispatchLoginEvent = () => {
|
||||||
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
||||||
navigate(redirectTo);
|
navigate(redirectTo);
|
||||||
@@ -415,7 +423,14 @@ export class LoginComponent extends Component {
|
|||||||
user: response.user
|
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 = () => {
|
const dispatchLoginEvent = () => {
|
||||||
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
window.dispatchEvent(new CustomEvent('userLoggedIn'));
|
||||||
navigate(redirectTo);
|
navigate(redirectTo);
|
||||||
|
|||||||
238
src/pages/LinkTelegramPage.js
Normal file
238
src/pages/LinkTelegramPage.js
Normal file
@@ -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 <LoginComponent open={showLogin} handleClose={handleLoginClose} location={location} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadingAuth) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 5 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="sm" sx={{ mt: 8, mb: 4 }}>
|
||||||
|
<Paper elevation={3} sx={{ p: 4 }}>
|
||||||
|
<Typography component="h1" variant="h5" gutterBottom>
|
||||||
|
Telegram verknüpfen
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Alert severity="error" sx={{ mt: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{success ? (
|
||||||
|
<Alert severity="success" sx={{ mt: 2 }}>
|
||||||
|
Telegram wurde erfolgreich verknüpft.
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{(!success && !error) || linking ? (
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Bitte warten, wir verifizieren und verknüpfen dein Telegram.
|
||||||
|
</Typography>
|
||||||
|
{linking ? <CircularProgress /> : null}
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => navigate('/profile#settings')}
|
||||||
|
sx={{ bgcolor: '#2e7d32', '&:hover': { bgcolor: '#1b5e20' } }}
|
||||||
|
disabled={linking}
|
||||||
|
>
|
||||||
|
Zurück zum Profil
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LinkTelegramPage;
|
||||||
|
|
||||||
Reference in New Issue
Block a user