aboutsummaryrefslogtreecommitdiff
path: root/modern/src/login
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2024-04-06 09:22:10 -0700
committerAnton Tananaev <anton@traccar.org>2024-04-06 09:22:10 -0700
commitf418231b6b2f5e030a0d2dcc390c314602b1f740 (patch)
tree10326adf3792bc2697e06bb5f2b8ef2a8f7e55fe /modern/src/login
parentb392a4af78e01c8e0f50aad5468e9583675b24be (diff)
downloadtrackermap-web-f418231b6b2f5e030a0d2dcc390c314602b1f740.tar.gz
trackermap-web-f418231b6b2f5e030a0d2dcc390c314602b1f740.tar.bz2
trackermap-web-f418231b6b2f5e030a0d2dcc390c314602b1f740.zip
Move modern to the top
Diffstat (limited to 'modern/src/login')
-rw-r--r--modern/src/login/ChangeServerPage.jsx83
-rw-r--r--modern/src/login/LoginLayout.jsx62
-rw-r--r--modern/src/login/LoginPage.jsx263
-rw-r--r--modern/src/login/LogoImage.jsx36
-rw-r--r--modern/src/login/RegisterPage.jsx148
-rw-r--r--modern/src/login/ResetPasswordPage.jsx118
6 files changed, 0 insertions, 710 deletions
diff --git a/modern/src/login/ChangeServerPage.jsx b/modern/src/login/ChangeServerPage.jsx
deleted file mode 100644
index 1ffc1257..00000000
--- a/modern/src/login/ChangeServerPage.jsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import React from 'react';
-import ElectricalServicesIcon from '@mui/icons-material/ElectricalServices';
-import { makeStyles } from '@mui/styles';
-import {
- Autocomplete, Button, Container, createFilterOptions, TextField,
-} from '@mui/material';
-import { useNavigate } from 'react-router-dom';
-import { useTranslation } from '../common/components/LocalizationProvider';
-
-const currentServer = `${window.location.protocol}//${window.location.host}`;
-
-const officialServers = [
- currentServer,
- 'https://demo.traccar.org',
- 'https://demo2.traccar.org',
- 'https://demo3.traccar.org',
- 'https://demo4.traccar.org',
- 'https://server.traccar.org',
- 'http://localhost:8082',
- 'http://localhost:3000',
-];
-
-const useStyles = makeStyles((theme) => ({
- icon: {
- textAlign: 'center',
- fontSize: '128px',
- color: theme.palette.neutral.main,
- },
- container: {
- textAlign: 'center',
- padding: theme.spacing(5, 3),
- },
- field: {
- margin: theme.spacing(3, 0),
- },
-}));
-
-const ChangeServerPage = () => {
- const classes = useStyles();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const filter = createFilterOptions();
-
- const handleSubmit = (url) => {
- if (window.webkit && window.webkit.messageHandlers.appInterface) {
- window.webkit.messageHandlers.appInterface.postMessage(`server|${url}`);
- } else if (window.appInterface) {
- window.appInterface.postMessage(`server|${url}`);
- } else {
- window.location.replace(url);
- }
- };
-
- return (
- <Container maxWidth="xs" className={classes.container}>
- <ElectricalServicesIcon className={classes.icon} />
- <Autocomplete
- freeSolo
- className={classes.field}
- options={officialServers}
- renderInput={(params) => <TextField {...params} label={t('settingsServer')} />}
- value={currentServer}
- onChange={(_, value) => value && handleSubmit(value)}
- filterOptions={(options, params) => {
- const filtered = filter(options, params);
- if (params.inputValue && !filtered.includes(params.inputValue)) {
- filtered.push(params.inputValue);
- }
- return filtered;
- }}
- />
- <Button
- onClick={() => navigate(-1)}
- color="secondary"
- >
- {t('sharedCancel')}
- </Button>
- </Container>
- );
-};
-
-export default ChangeServerPage;
diff --git a/modern/src/login/LoginLayout.jsx b/modern/src/login/LoginLayout.jsx
deleted file mode 100644
index 8f2ee6ca..00000000
--- a/modern/src/login/LoginLayout.jsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React from 'react';
-import { useMediaQuery, Paper } from '@mui/material';
-import makeStyles from '@mui/styles/makeStyles';
-import { useTheme } from '@mui/material/styles';
-import LogoImage from './LogoImage';
-
-const useStyles = makeStyles((theme) => ({
- root: {
- display: 'flex',
- height: '100%',
- },
- sidebar: {
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- background: theme.palette.primary.main,
- paddingBottom: theme.spacing(5),
- width: theme.dimensions.sidebarWidth,
- [theme.breakpoints.down('lg')]: {
- width: theme.dimensions.sidebarWidthTablet,
- },
- [theme.breakpoints.down('sm')]: {
- width: '0px',
- },
- },
- paper: {
- display: 'flex',
- flexDirection: 'column',
- justifyContent: 'center',
- alignItems: 'center',
- flex: 1,
- boxShadow: '-2px 0px 16px rgba(0, 0, 0, 0.25)',
- [theme.breakpoints.up('lg')]: {
- padding: theme.spacing(0, 25, 0, 0),
- },
- },
- form: {
- maxWidth: theme.spacing(52),
- padding: theme.spacing(5),
- width: '100%',
- },
-}));
-
-const LoginLayout = ({ children }) => {
- const classes = useStyles();
- const theme = useTheme();
-
- return (
- <main className={classes.root}>
- <div className={classes.sidebar}>
- {!useMediaQuery(theme.breakpoints.down('lg')) && <LogoImage color={theme.palette.secondary.contrastText} />}
- </div>
- <Paper className={classes.paper}>
- <form className={classes.form}>
- {children}
- </form>
- </Paper>
- </main>
- );
-};
-
-export default LoginLayout;
diff --git a/modern/src/login/LoginPage.jsx b/modern/src/login/LoginPage.jsx
deleted file mode 100644
index 27aad7c9..00000000
--- a/modern/src/login/LoginPage.jsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import dayjs from 'dayjs';
-import {
- useMediaQuery, Select, MenuItem, FormControl, Button, TextField, Link, Snackbar, IconButton, Tooltip, LinearProgress, Box,
-} from '@mui/material';
-import ReactCountryFlag from 'react-country-flag';
-import makeStyles from '@mui/styles/makeStyles';
-import CloseIcon from '@mui/icons-material/Close';
-import LockOpenIcon from '@mui/icons-material/LockOpen';
-import { useTheme } from '@mui/material/styles';
-import { useDispatch, useSelector } from 'react-redux';
-import { useNavigate } from 'react-router-dom';
-import { sessionActions } from '../store';
-import { useLocalization, useTranslation } from '../common/components/LocalizationProvider';
-import LoginLayout from './LoginLayout';
-import usePersistedState from '../common/util/usePersistedState';
-import { handleLoginTokenListeners, nativeEnvironment, nativePostMessage } from '../common/components/NativeInterface';
-import LogoImage from './LogoImage';
-import { useCatch } from '../reactHelper';
-
-const useStyles = makeStyles((theme) => ({
- options: {
- position: 'fixed',
- top: theme.spacing(2),
- right: theme.spacing(2),
- display: 'flex',
- flexDirection: 'row',
- gap: theme.spacing(1),
- },
- container: {
- display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(2),
- },
- extraContainer: {
- display: 'flex',
- flexDirection: 'row',
- justifyContent: 'center',
- gap: theme.spacing(4),
- marginTop: theme.spacing(2),
- },
- registerButton: {
- minWidth: 'unset',
- },
- link: {
- cursor: 'pointer',
- },
-}));
-
-const LoginPage = () => {
- const classes = useStyles();
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
- const t = useTranslation();
-
- const { languages, language, setLanguage } = useLocalization();
- const languageList = Object.entries(languages).map((values) => ({ code: values[0], country: values[1].country, name: values[1].name }));
-
- const [failed, setFailed] = useState(false);
-
- const [email, setEmail] = usePersistedState('loginEmail', '');
- const [password, setPassword] = useState('');
- const [code, setCode] = useState('');
-
- const registrationEnabled = useSelector((state) => state.session.server.registration);
- const languageEnabled = useSelector((state) => !state.session.server.attributes['ui.disableLoginLanguage']);
- const changeEnabled = useSelector((state) => !state.session.server.attributes.disableChange);
- const emailEnabled = useSelector((state) => state.session.server.emailEnabled);
- const openIdEnabled = useSelector((state) => state.session.server.openIdEnabled);
- const openIdForced = useSelector((state) => state.session.server.openIdEnabled && state.session.server.openIdForce);
- const [codeEnabled, setCodeEnabled] = useState(false);
-
- const [announcementShown, setAnnouncementShown] = useState(false);
- const announcement = useSelector((state) => state.session.server.announcement);
-
- const generateLoginToken = async () => {
- if (nativeEnvironment) {
- let token = '';
- try {
- const expiration = dayjs().add(6, 'months').toISOString();
- const response = await fetch('/api/session/token', {
- method: 'POST',
- body: new URLSearchParams(`expiration=${expiration}`),
- });
- if (response.ok) {
- token = await response.text();
- }
- } catch (error) {
- token = '';
- }
- nativePostMessage(`login|${token}`);
- }
- };
-
- const handlePasswordLogin = async (event) => {
- event.preventDefault();
- setFailed(false);
- try {
- const query = `email=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`;
- const response = await fetch('/api/session', {
- method: 'POST',
- body: new URLSearchParams(code.length ? `${query}&code=${code}` : query),
- });
- if (response.ok) {
- const user = await response.json();
- generateLoginToken();
- dispatch(sessionActions.updateUser(user));
- navigate('/');
- } else if (response.status === 401 && response.headers.get('WWW-Authenticate') === 'TOTP') {
- setCodeEnabled(true);
- } else {
- throw Error(await response.text());
- }
- } catch (error) {
- setFailed(true);
- setPassword('');
- }
- };
-
- const handleTokenLogin = useCatch(async (token) => {
- const response = await fetch(`/api/session?token=${encodeURIComponent(token)}`);
- if (response.ok) {
- const user = await response.json();
- dispatch(sessionActions.updateUser(user));
- navigate('/');
- } else {
- throw Error(await response.text());
- }
- });
-
- const handleOpenIdLogin = () => {
- document.location = '/api/session/openid/auth';
- };
-
- useEffect(() => nativePostMessage('authentication'), []);
-
- useEffect(() => {
- const listener = (token) => handleTokenLogin(token);
- handleLoginTokenListeners.add(listener);
- return () => handleLoginTokenListeners.delete(listener);
- }, []);
-
- if (openIdForced) {
- handleOpenIdLogin();
- return (<LinearProgress />);
- }
-
- return (
- <LoginLayout>
- <div className={classes.options}>
- {nativeEnvironment && changeEnabled && (
- <Tooltip title={t('settingsServer')}>
- <IconButton onClick={() => navigate('/change-server')}>
- <LockOpenIcon />
- </IconButton>
- </Tooltip>
- )}
- {languageEnabled && (
- <FormControl>
- <Select value={language} onChange={(e) => setLanguage(e.target.value)}>
- {languageList.map((it) => (
- <MenuItem key={it.code} value={it.code}>
- <Box component="span" sx={{ mr: 1 }}>
- <ReactCountryFlag countryCode={it.country} svg />
- </Box>
- {it.name}
- </MenuItem>
- ))}
- </Select>
- </FormControl>
- )}
- </div>
- <div className={classes.container}>
- {useMediaQuery(theme.breakpoints.down('lg')) && <LogoImage color={theme.palette.primary.main} />}
- <TextField
- required
- error={failed}
- label={t('userEmail')}
- name="email"
- value={email}
- autoComplete="email"
- autoFocus={!email}
- onChange={(e) => setEmail(e.target.value)}
- helperText={failed && 'Invalid username or password'}
- />
- <TextField
- required
- error={failed}
- label={t('userPassword')}
- name="password"
- value={password}
- type="password"
- autoComplete="current-password"
- autoFocus={!!email}
- onChange={(e) => setPassword(e.target.value)}
- />
- {codeEnabled && (
- <TextField
- required
- error={failed}
- label={t('loginTotpCode')}
- name="code"
- value={code}
- type="number"
- onChange={(e) => setCode(e.target.value)}
- />
- )}
- <Button
- onClick={handlePasswordLogin}
- type="submit"
- variant="contained"
- color="secondary"
- disabled={!email || !password || (codeEnabled && !code)}
- >
- {t('loginLogin')}
- </Button>
- {openIdEnabled && (
- <Button
- onClick={() => handleOpenIdLogin()}
- variant="contained"
- color="secondary"
- >
- {t('loginOpenId')}
- </Button>
- )}
- <div className={classes.extraContainer}>
- {registrationEnabled && (
- <Link
- onClick={() => navigate('/register')}
- className={classes.link}
- underline="none"
- variant="caption"
- >
- {t('loginRegister')}
- </Link>
- )}
- {emailEnabled && (
- <Link
- onClick={() => navigate('/reset-password')}
- className={classes.link}
- underline="none"
- variant="caption"
- >
- {t('loginReset')}
- </Link>
- )}
- </div>
- </div>
- <Snackbar
- open={!!announcement && !announcementShown}
- message={announcement}
- action={(
- <IconButton size="small" color="inherit" onClick={() => setAnnouncementShown(true)}>
- <CloseIcon fontSize="small" />
- </IconButton>
- )}
- />
- </LoginLayout>
- );
-};
-
-export default LoginPage;
diff --git a/modern/src/login/LogoImage.jsx b/modern/src/login/LogoImage.jsx
deleted file mode 100644
index e92403ef..00000000
--- a/modern/src/login/LogoImage.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import { useTheme, useMediaQuery } from '@mui/material';
-import { useSelector } from 'react-redux';
-import { makeStyles } from '@mui/styles';
-import Logo from '../resources/images/logo.svg?react';
-
-const useStyles = makeStyles((theme) => ({
- image: {
- alignSelf: 'center',
- maxWidth: '240px',
- maxHeight: '120px',
- width: 'auto',
- height: 'auto',
- margin: theme.spacing(2),
- },
-}));
-
-const LogoImage = ({ color }) => {
- const theme = useTheme();
- const classes = useStyles();
-
- const expanded = !useMediaQuery(theme.breakpoints.down('lg'));
-
- const logo = useSelector((state) => state.session.server.attributes?.logo);
- const logoInverted = useSelector((state) => state.session.server.attributes?.logoInverted);
-
- if (logo) {
- if (expanded && logoInverted) {
- return <img className={classes.image} src={logoInverted} alt="" />;
- }
- return <img className={classes.image} src={logo} alt="" />;
- }
- return <Logo className={classes.image} style={{ color }} />;
-};
-
-export default LogoImage;
diff --git a/modern/src/login/RegisterPage.jsx b/modern/src/login/RegisterPage.jsx
deleted file mode 100644
index 4e2a05e1..00000000
--- a/modern/src/login/RegisterPage.jsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import React, { useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import {
- Button, TextField, Typography, Snackbar, IconButton,
-} from '@mui/material';
-import makeStyles from '@mui/styles/makeStyles';
-import { useNavigate } from 'react-router-dom';
-import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-import LoginLayout from './LoginLayout';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import { snackBarDurationShortMs } from '../common/util/duration';
-import { useCatch, useEffectAsync } from '../reactHelper';
-import { sessionActions } from '../store';
-
-const useStyles = makeStyles((theme) => ({
- container: {
- display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(2),
- },
- header: {
- display: 'flex',
- alignItems: 'center',
- },
- title: {
- fontSize: theme.spacing(3),
- fontWeight: 500,
- marginLeft: theme.spacing(1),
- textTransform: 'uppercase',
- },
-}));
-
-const RegisterPage = () => {
- const classes = useStyles();
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const t = useTranslation();
-
- const server = useSelector((state) => state.session.server);
- const totpForce = useSelector((state) => state.session.server.attributes.totpForce);
-
- const [name, setName] = useState('');
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [totpKey, setTotpKey] = useState(null);
- const [snackbarOpen, setSnackbarOpen] = useState(false);
-
- useEffectAsync(async () => {
- if (totpForce) {
- const response = await fetch('/api/users/totp', { method: 'POST' });
- if (response.ok) {
- setTotpKey(await response.text());
- } else {
- throw Error(await response.text());
- }
- }
- }, [totpForce, setTotpKey]);
-
- const handleSubmit = useCatch(async (event) => {
- event.preventDefault();
- const response = await fetch('/api/users', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, email, password, totpKey }),
- });
- if (response.ok) {
- setSnackbarOpen(true);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <LoginLayout>
- <div className={classes.container}>
- <div className={classes.header}>
- {!server.newServer && (
- <IconButton color="primary" onClick={() => navigate('/login')}>
- <ArrowBackIcon />
- </IconButton>
- )}
- <Typography className={classes.title} color="primary">
- {t('loginRegister')}
- </Typography>
- </div>
- <TextField
- required
- label={t('sharedName')}
- name="name"
- value={name}
- autoComplete="name"
- autoFocus
- onChange={(event) => setName(event.target.value)}
- />
- <TextField
- required
- type="email"
- label={t('userEmail')}
- name="email"
- value={email}
- autoComplete="email"
- onChange={(event) => setEmail(event.target.value)}
- />
- <TextField
- required
- label={t('userPassword')}
- name="password"
- value={password}
- type="password"
- autoComplete="current-password"
- onChange={(event) => setPassword(event.target.value)}
- />
- {totpForce && (
- <TextField
- required
- label={t('loginTotpKey')}
- name="totpKey"
- value={totpKey || ''}
- InputProps={{
- readOnly: true,
- }}
- />
- )}
- <Button
- variant="contained"
- color="secondary"
- onClick={handleSubmit}
- type="submit"
- disabled={!name || !password || !(server.newServer || /(.+)@(.+)\.(.{2,})/.test(email))}
- fullWidth
- >
- {t('loginRegister')}
- </Button>
- </div>
- <Snackbar
- open={snackbarOpen}
- onClose={() => {
- dispatch(sessionActions.updateServer({ ...server, newServer: false }));
- navigate('/login');
- }}
- autoHideDuration={snackBarDurationShortMs}
- message={t('loginCreated')}
- />
- </LoginLayout>
- );
-};
-
-export default RegisterPage;
diff --git a/modern/src/login/ResetPasswordPage.jsx b/modern/src/login/ResetPasswordPage.jsx
deleted file mode 100644
index d10299ca..00000000
--- a/modern/src/login/ResetPasswordPage.jsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import React, { useState } from 'react';
-import {
- Button, TextField, Typography, Snackbar, IconButton,
-} from '@mui/material';
-import makeStyles from '@mui/styles/makeStyles';
-import { useNavigate } from 'react-router-dom';
-import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-import LoginLayout from './LoginLayout';
-import { useTranslation } from '../common/components/LocalizationProvider';
-import useQuery from '../common/util/useQuery';
-import { snackBarDurationShortMs } from '../common/util/duration';
-import { useCatch } from '../reactHelper';
-
-const useStyles = makeStyles((theme) => ({
- container: {
- display: 'flex',
- flexDirection: 'column',
- gap: theme.spacing(2),
- },
- header: {
- display: 'flex',
- alignItems: 'center',
- },
- title: {
- fontSize: theme.spacing(3),
- fontWeight: 500,
- marginLeft: theme.spacing(1),
- textTransform: 'uppercase',
- },
-}));
-
-const ResetPasswordPage = () => {
- const classes = useStyles();
- const navigate = useNavigate();
- const t = useTranslation();
- const query = useQuery();
-
- const token = query.get('passwordReset');
-
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [snackbarOpen, setSnackbarOpen] = useState(false);
-
- const handleSubmit = useCatch(async (event) => {
- event.preventDefault();
- let response;
- if (!token) {
- response = await fetch('/api/password/reset', {
- method: 'POST',
- body: new URLSearchParams(`email=${encodeURIComponent(email)}`),
- });
- } else {
- response = await fetch('/api/password/update', {
- method: 'POST',
- body: new URLSearchParams(`token=${encodeURIComponent(token)}&password=${encodeURIComponent(password)}`),
- });
- }
- if (response.ok) {
- setSnackbarOpen(true);
- } else {
- throw Error(await response.text());
- }
- });
-
- return (
- <LoginLayout>
- <div className={classes.container}>
- <div className={classes.header}>
- <IconButton color="primary" onClick={() => navigate('/login')}>
- <ArrowBackIcon />
- </IconButton>
- <Typography className={classes.title} color="primary">
- {t('loginReset')}
- </Typography>
- </div>
- {!token ? (
- <TextField
- required
- type="email"
- label={t('userEmail')}
- name="email"
- value={email}
- autoComplete="email"
- onChange={(event) => setEmail(event.target.value)}
- />
- ) : (
- <TextField
- required
- label={t('userPassword')}
- name="password"
- value={password}
- type="password"
- autoComplete="current-password"
- onChange={(event) => setPassword(event.target.value)}
- />
- )}
- <Button
- variant="contained"
- color="secondary"
- type="submit"
- onClick={handleSubmit}
- disabled={!/(.+)@(.+)\.(.{2,})/.test(email) && !password}
- fullWidth
- >
- {t('loginReset')}
- </Button>
- </div>
- <Snackbar
- open={snackbarOpen}
- onClose={() => navigate('/login')}
- autoHideDuration={snackBarDurationShortMs}
- message={!token ? t('loginResetSuccess') : t('loginUpdateSuccess')}
- />
- </LoginLayout>
- );
-};
-
-export default ResetPasswordPage;