feat: Add LinkTelegram page and routing; enhance login flow to support redirection from linkTelegram

This commit is contained in:
sebseb7
2026-03-27 01:29:04 +01:00
parent 5b7f0f788c
commit 7202c43dfa
5 changed files with 265 additions and 3 deletions

View File

@@ -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 */}
<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 */}
<Route path="/payment/success" element={<PaymentSuccess />} />

View File

@@ -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);

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