feat: Add LinkTelegram page and routing; enhance login flow to support redirection from linkTelegram
This commit is contained in:
@@ -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 />} />
|
||||
|
||||
@@ -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);
|
||||
|
||||
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