Files
dashboard/frontend/src/components/auth/LoginCallback.tsx
Matthias Hochmeister 93a87a7ae9 apply security audit
2026-03-11 13:18:10 +01:00

111 lines
3.0 KiB
TypeScript

import React, { useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { Box, CircularProgress, Typography, Alert, Button } from '@mui/material';
const LoginCallback: React.FC = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { login } = useAuth();
const [error, setError] = useState<string>('');
const hasCalledLogin = useRef(false);
useEffect(() => {
if (hasCalledLogin.current) return;
hasCalledLogin.current = true;
const handleCallback = async () => {
const code = searchParams.get('code');
const errorParam = searchParams.get('error');
if (errorParam) {
setError(`Authentifizierungsfehler: ${errorParam}`);
return;
}
if (!code) {
setError('Kein Autorisierungscode erhalten');
return;
}
try {
await login(code);
// Navigate to the originally intended page, falling back to the dashboard.
// Validate that the stored path is a safe internal path: must start with '/'
// but must NOT start with '//' (protocol-relative redirect).
const rawFrom = sessionStorage.getItem('auth_redirect_from');
const from =
rawFrom && rawFrom.startsWith('/') && !rawFrom.startsWith('//')
? rawFrom
: '/dashboard';
sessionStorage.removeItem('auth_redirect_from');
navigate(from, { replace: true });
} catch (err) {
console.error('Login callback error:', err);
const is429 = err && typeof err === 'object' && 'status' in err && (err as any).status === 429;
setError(
is429
? 'Zu viele Anmeldeversuche. Bitte warten Sie einige Minuten und versuchen Sie es erneut.'
: err instanceof Error
? err.message
: 'Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.'
);
}
};
handleCallback();
}, [searchParams, login, navigate]);
if (error) {
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
padding: 3,
}}
>
<Alert
severity="error"
sx={{
maxWidth: 500,
mb: 2,
width: '100%',
}}
>
{error}
</Alert>
<Button
variant="contained"
onClick={() => navigate('/login')}
>
Zurück zur Anmeldung
</Button>
</Box>
);
}
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: 2,
}}
>
<CircularProgress size={60} />
<Typography variant="h6" color="text.secondary">
Anmeldung wird abgeschlossen...
</Typography>
</Box>
);
};
export default LoginCallback;