Files
dashfront/src/components/LoginPage.jsx
2026-10-05 15:56:41 +02:00

163 lines
4.5 KiB
JavaScript

import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import GoogleIcon from '@mui/icons-material/Google';
import { enqueueSnackbar } from 'notistack';
import { notifyErrorMessage } from '../utils/notify';
import authAutomat, { bootstrapAuthMe } from '../automats/authAutomat';
import {
abortGoogleSignIn,
FEDCM_BLOCKED_MESSAGE,
GoogleSignInBlockedError,
GoogleSignInCancelledError,
logError,
requestGoogleIdToken,
} from '../auth/googleSignIn';
import quixposLogo from '../assets/quixpos-logo.png';
import {
NAV_SURFACE,
} from '../colors';
class LoginPage extends PureComponent {
constructor(props) {
super(props);
this.state = { ...authAutomat.state, busy: false };
this.handleLogin = this.handleLogin.bind(this);
}
componentDidMount() {
document.title = 'QuixPOS * Management Interface Login';
this.unsubscribe = authAutomat.subscribe(this, ['status', 'googleClientId']);
}
componentWillUnmount() {
this.unsubscribe?.();
}
async handleLogin() {
const { googleClientId } = this.state;
abortGoogleSignIn();
try {
const idToken = await requestGoogleIdToken(googleClientId, {
onFedcmBlocked: () => {
enqueueSnackbar(FEDCM_BLOCKED_MESSAGE, {
variant: 'warning',
autoHideDuration: 12000,
});
},
});
this.setState({ busy: true });
await authAutomat.actions.loginWithGoogle(idToken);
await bootstrapAuthMe();
} catch (err) {
if (err instanceof GoogleSignInCancelledError) return;
logError('[login] Google sign-in failed', {
message: err?.message,
name: err?.name,
code: err?.code,
status: err?.status,
cause: err?.cause,
stack: err?.stack,
clientId: googleClientId,
origin: window.location.origin,
href: window.location.href,
dashHost: window.__dashHost
? {
isDashNative: window.__dashHost.isDashNative,
googleSignIn: window.__dashHost.googleSignIn,
}
: null,
error: err,
});
if (err instanceof GoogleSignInBlockedError) {
enqueueSnackbar(err.message, { variant: 'warning', autoHideDuration: 12000 });
return;
}
const detail = [err?.message || String(err), err?.code, err?.status]
.filter(Boolean)
.join(' · ');
notifyErrorMessage(`Login failed: ${detail}`);
} finally {
this.setState({ busy: false });
}
}
render() {
const { busy, status, googleClientId } = this.state;
const loading = status === 'loading';
const ready = Boolean(googleClientId);
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
bgcolor: 'background.default',
p: 2,
}}
>
<Paper
elevation={3}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 2.5,
px: 5,
py: 5,
maxWidth: 400,
width: '100%',
borderRadius: 3,
textAlign: 'center',
}}
>
<Box
component="img"
src={quixposLogo}
alt="QuixPOS Logo"
sx={{
width: 96,
height: 96,
borderRadius: 3,
display: 'block',
}}
/>
<Box>
<Typography
variant="h5"
component="h1"
sx={{ fontWeight: 700, color: NAV_SURFACE, letterSpacing: '-0.01em' }}
>
QuixPOS
</Typography>
<Typography
variant="subtitle1"
sx={{ color: 'text.secondary', fontWeight: 500, mt: 0.5 }}
>
Management Interface Login
</Typography>
</Box>
<Button
variant="contained"
size="large"
fullWidth
startIcon={<GoogleIcon />}
disabled={busy || loading || !ready}
onClick={this.handleLogin}
sx={{ mt: 1, py: 1.25, textTransform: 'none', fontWeight: 600 }}
>
{busy ? 'Anmeldung läuft…' : 'Mit Google anmelden'}
</Button>
</Paper>
</Box>
);
}
}
export default LoginPage;