diff options
Diffstat (limited to 'modern/src/login')
-rw-r--r-- | modern/src/login/ChangeServerPage.jsx | 83 | ||||
-rw-r--r-- | modern/src/login/LoginLayout.jsx | 62 | ||||
-rw-r--r-- | modern/src/login/LoginPage.jsx | 263 | ||||
-rw-r--r-- | modern/src/login/LogoImage.jsx | 36 | ||||
-rw-r--r-- | modern/src/login/RegisterPage.jsx | 148 | ||||
-rw-r--r-- | modern/src/login/ResetPasswordPage.jsx | 118 |
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; |